Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.47.0-wmf.20
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Event
Event talk
MediaWiki:Gadgets-definition
8
23707
772494
707697
2026-09-20T10:03:51Z
NguoiDungKhongDinhDanh
106900
Remove deprecated option. See [[:phab:T298199|T298199]] for more information. ([[:m:GS|global sysop]] action)
772494
wikitext
text/x-wiki
# Cette page sert à définir les gadgets utilisés dans [[Special:Gadgets]]
# Avant de la modifier, assurez-vous d'avoir compris son fonctionnement décrit sur [[mw:Extension:Gadgets#Usage]]
# Ne changez pas le nom d'un gadget existant (il serait alors désactivé pour tous)
# Si un gadget ne fonctionne pas, on peut le commenter en transformant * en #
# La version actuelle de MediaWiki ne supporte pas les noms de gadget et section comportant des caractères spéciaux ou accentués
== Présentation ==
* LeftPaneSwitch|LeftPaneSwitch.js
* CoinsArrondis|CoinsArrondis.css
* ListeABordure|ListeABordure.css
* UnicodeEditRendering|UnicodeEditRendering.css
* FlecheHaut|FlecheHaut.js
* AncreTitres|AncreTitres.js
* DirectPageLink|Common.js|DirectPageLink.js
* autonum|autonum.css|autonum.js
* TitreHierarchique|CommonClass.js|CommonDom.js|TitreHierarchique.js
* TitreDeluxe|TitreDeluxe.js
* Logo|Common.js|Logo.js
* CouleurContributions|CouleurContributions.js
* extlinks[default|dependencies=mediawiki.util]|extlinks.js
* JournalEnTable|JournalEnTable.css|CommonDom.js|JournalEnTable.js
== Outils ==
* UTCLiveClock[dependencies=mediawiki.util]|UTCLiveClock.js
* LocalLiveClock[dependencies=mediawiki.util]|LocalLiveClock.js
* DeluxeHistory[dependencies=user,mediawiki.util]|DeluxeHistory.js
* SousPages[dependencies=mediawiki.util]|SousPages.js
* searchFocus|searchFocus.js
* perpagecustomization|perpagecustomization.js
* LiveRC|LiveRC.js
* recentchangesbox[skins=monobook]|recentchangesbox.js
* MobileView|CommonDom.js|MobileView.js
* SkinPreview[dependencies=mediawiki.util]|SkinPreview.js
== Onglets ==
* OngletEditZeroth[dependencies=mediawiki.util]|OngletEditZeroth.js
* OngletPurge|Common.js|CommonDom.js|OngletPurge.js
* OngletEditCount|CommonDom.js|OngletEditCount.js
* OngletGoogle|CommonDom.js|OngletGoogle.js
* SisterProjects[dependencies=user,mediawiki.utils]|SisterProjects.js
* monBrouillon[dependencies=mediawiki.util]|monBrouillon.js
* Smart_patrol [rights=patrol] | Smart_patrol.js
== Script ==
* ScriptToolbar|ScriptToolbar.css|Common.js|CommonEdit.js|ScriptToolbar.js
* ScriptSidebox[dependencies=mediawiki.util]|ScriptSidebox.js
* CouleursLiens|CouleursLiens.js
* CategorySeparator|CategorySeparator.js
== Développeurs ==
* ScriptAutoVersion|Common.js|ScriptAutoVersion.js
* JournalDebug|CommonDom.js|JournalDebug.js
* DevTools|Common.js|CommonDom.js|JournalDebug.js|DevTools.css|DevTools.js
== Édition ==
* Tableau[rights=edit]|Tableau.js
* FixArrayAltLines[rights=edit]|Common.js|CommonEdit.js|FixArrayAltLines.js
* TableUnicode[rights=edit]|TableUnicode.js
* BoutonsLiens[rights=edit]|Common.js|CommonEdit.js|ReplaceLink.js|BoutonsLiens.js
* Emoticons[rights=edit]|Common.js|CommonEdit.js|EmoticonsCommon.js|Emoticons.js
* EmoticonsToolbar[rights=edit]|ScriptToolbar.css|Common.js|CommonEdit.js|ScriptToolbar.js|EmoticonsCommon.js|EmoticonsToolbar.js
* SpaceToolbar[rights=edit|dependencies=mediawiki.cookie]|ScriptToolbar.css|Common.js|CommonEdit.js|ScriptToolbar.js|SpaceFormat.js
* SourceLanguage[rights=edit|dependencies=mediawiki.cookie]|ScriptToolbar.css|Common.js|CommonEdit.js|CommonDom.js|ScriptToolbar.js|SourceLanguage.js
* Barre de luxe[dependencies=ext.gadget.mediawiki.toolbar|rights=edit]|Common.js|Barre de luxe.js
* OptimizedSuivi[skins=monobook]|CommonDom.js|OptimizedSuivi.js
* DeluxeSummary[rights=edit]|CommonDom.js|DeluxeSummary.js
* searchbox|lib-beau.js|searchbox.js
* WikEd[rights=edit]|WikEd.js
== Avancé ==
* HotCats[rights=edit]|Common.js|HotCats.js
* Popups|Popups.js
* DeluxeEdit|DeluxeEdit.js
* DeluxeRename[rights=move]|Common.js|DeluxeRename.js
* RenommageCategorie[rights=delete]|Common.js|RenommageCategorie.js
* DeluxeImport[rights=import]|Common.js|DeluxeImport.js
* DeleteSetup[default|rights=delete]|DeleteSetup.js
* DeluxeAdmin[rights=delete]|Common.js|DeluxeAdmin.js
* RestaurationDeluxe[rights=delete]|RestaurationDeluxe.js
* NavigAdmin|CommonDom.js|NavigAdmin.js
* GoogleTrans|GoogleTrans.js
* RevertDiff[rights=edit]|CommonDom.js|RevertDiff.js
* FastRevert[rights=edit]|CommonDom.js|FastRevert.js
* massblock[rights=block]|CommonDom.js|massblock.js|blockoptions.js
* mediawiki.toolbar [hidden] | mediawiki.toolbar.js
== Test ==
* ArchiveLinks|ArchiveLinks.js
* CategoryAboveAll|CategoryAboveAll.js
* CollapseSidebox[dependencies=mediawiki.cookie]|Common.js|CollapseSidebox.js
# Importés dans common.js :
# Wikiwix[default]|Wikiwix.js
# IntersectionCategorie[default|dependencies=mediawiki.util]|IntersectionCategorie.js
tjthtjjvhn4rqswx9xpllpwavkplvxx
Grec ancien/Lexique
0
29558
772491
771631
2026-09-20T08:42:34Z
~2026-50625-01
124585
/* Χ */
772491
wikitext
text/x-wiki
{{Grec ancien}}
{{Wiktionnaire|Catégorie:grec ancien}}
Au cours de vos lectures, vous serez amené à rencontrer des mots que vous n’arriverez pas à traduire en français. Le mieux est d’avoir un dictionnaire grec-français (type Bailly) mais certains mots reviennent souvent ; voici un lexique non-exhaustif grec-français par ordre alphabétique avec des renseignements sur le mot (genre, déclinaison, etc.)
[[Fichier:NAMA Stèle d'Hègèsô.jpg|left|170px|thumb|Stèle funéraire en marbre trouvée à Athènes. La défunte est Hègèsô, fille de Proxenos : la qualité du travail indique une famille noble. L’œuvre a été attribuée au sculpteur Callimaque. Musée archéologique d’Athènes, Grèce.]]
__FORCERSOMMAIRE__
==Α==
'''ἆ (interjection)''' : ah !<br>
'''ἃ ἅ (interjection)''' : ha ha.<br>
'''ἇ ἇ (interjection)''' : .<br>
'''ἀ- (préfixe)''' (Devient ''ἀν-'' devant un mot commençant par une voyelle.) : préfixe privatif.<br>
'''ἀάω (verbe)''' : Blesser, frapper. Frapper l’esprit. Égarer, tromper. S’égarer, commettre une faute.<br>
'''ἀϐαθέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἀϐαθής''.<br>
'''ἀϐαθέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''ἀϐαθής''.<br>
'''ἀϐαθής, -ής, -ές (adjectif)''' : superficiel.<br>
'''ἀϐαθῶς (adverbe)''' : superficiellement.<br>
'''ἀϐαρέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἀϐαρής''.<br>
'''ἀϐαρέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''ἀϐαρής''.<br>
'''ἀϐαρής, -ής, -ές (adjectif)''' : léger.<br>
'''ἀϐαρῶς (adverbe)''' : légèrement.<br>
'''ἄϐαξ, -κος (nom commun) (m)''' : Planche, tablette. Tableau de mathématicien. Table à jouer. Plat, assiette. Table pour compter les votes.<br>
'''ἀϐϐᾶς, -ᾶ (nom commun) (m)''' : abbé.<br>
'''αϐϐαεῖον, -ίου (nom commun) (n)''' : abbaye.<br>
'''ἀϐέλιος, -ίου (nom commun) (m)''' : Forme crétoise de ''ἥλιος''.<br>
'''ἀϐλαϐής, -ής, -ές (adjectif)''' : Inoffensif ; sûr.<br>
'''ἁϐός, -ή, -όν (adjectif)''' : Forme dorienne de ''ἡϐός''.<br>
'''ἁϐότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἁϐός''.<br>
'''ἁϐότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἁϐός''.<br>
'''ἁϐρός, -ά, -όν (adjectif)''' : tendre ; délicat.<br>
'''ἁϐρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἁϐρός''.<br>
'''ἁϐρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἁϐρός''.<br>
'''ἁϐρότης, -τος (nom commun) (f)''' : Bonheur, prospérité. Affectation. Magnificence, faste, opulence.<br>
'''ἁϐρῶς (adverbe)''' : tendrement ; délicatement.<br>
'''ἀγαθοεργία, -ας (nom commun) (f)''' : charité.<br>
'''ἀγαθός, -ή, -όν (adjectif)''' : Bon ; utile.<br>
'''ἀγαθωσύνη, -ης (nom commun) (f)''' : bonté, bienveillance.<br>
'''ἀγαθῶς (adverbe)''' : Bonnement ; utilement.<br>
'''ἀγαλλίασις, -άσεως (nom commun) (f)''' : jubilation.<br>
'''ἀγάλλω (verbe)''' : jubiler.<br>
'''ἄγαλμα, -άλματος (nom commun) (n)''' : statue.<br>
'''ἄγαν (adverbe)''' : trop.<br>
'''ἀγάπη, -ης (nom commun) (f)''' : amour divin, universel, inconditionnel.<br>
'''ἀγαπῶ (verbe)''' : aimer d’amour.<br>
'''ἀγασός, -ή, -όν (adjectif)''' : Forme dorienne de ''ἀγαθός''.<br>
'''ἀγγελιαφόρος, -ου (nom commun) (m)''' : messager.<br>
'''ἀγγελικός, -ή, -όν (adjectif)''' : d’ange.<br>
'''ἀγγελικῶς (adverbe)''' : angéliquement.<br>
'''ἀγγελικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀγγελικός''.<br>
'''ἀγγελικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀγγελικός''.<br>
'''ἄγγελος, -έλου (nom commun) (m)''' : messager ; ange.<br>
'''ἀγγέλλω (verbe)''' : annoncer.<br>
'''ἀγγίζω (verbe)''' : toucher.<br>
'''ἀγείρω (verbe)''' : assembler, rassembler.<br>
'''ἀγελάζω (verbe)''' : rassembler en troupeau.<br>
'''ἀγελαία, -ας (nom commun) (f)''' : .<br>
'''ἀγελαῖος, -ία, -ῖον (adjectif)''' : .<br>
'''ἀγελάς, -δος (nom commun) (f)''' : .<br>
'''ἀγέλαστος, -ος, -ον (adjectif)''' : maussade ; sombre.<br>
'''ἀγέλη, -ης (nom commun) (f)''' : troupeau.<br>
'''ἁγεμών, -όνος (nom commun) (m)''' : Forme dorienne de ''ἡγεμών''.<br>
'''ἁγίμων, -όνος (nom commun) (m)''' : Forme éolienne de ''ἡγεμών''.<br>
'''ἁγιάζω (verbe)''' : Bénir ; consacrer.<br>
'''ἁγιασμός, -οῦ (nom commun) (m)''' : bénédiction.<br>
'''ἅγιος, -ία, -ιον (adjectif)''' : auguste, sacré ; saint.<br>
'''ἁγίως (adverbe)''' : saintement.<br>
'''ἁγιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἅγιος''.<br>
'''ἁγιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἅγιος''.<br>
'''ἄγκιστρον, -ίστρου (nom commun) (n)''' : crochet.<br>
'''ἀγκυλίς, -δος (nom commun) (f)''' : crochet.<br>
'''ἀγκύλος, -η, -ον (adjectif)''' : courbé.<br>
'''ἀγκύλως (adverbe)''' : .<br>
'''ἀγκυλώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀγκύλος''.<br>
'''ἀγκυλώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀγκύλος''.<br>
'''ἄγκυρα, -ύρας (nom commun) (f)''' : ancre.<br>
'''ἀγκών, -ῶνος (nom commun) (m)''' : coude.<br>
'''ἀγλαός, -ός, -όν (adjectif)''' : brillant.<br>
'''ἁγνεία, -ας (nom commun) (f)''' : chasteté ; pureté. Purification ; raffinage<br>
'''ἁγνίζω (verbe)''' : être chaste.<br>
'''ἅγνισμα, -τος (nom commun) (n)''' : objet d'un sacrifice (ou offrande).<br>
'''ἁγνιστής, -οῦ (nom commun) (m)''' : .<br>
'''ἀγνόημα, -ήματος (nom commun) (n)''' : omission.<br>
'''ἄγνοια, -ίας (nom commun) (f)''' : ignorance.<br>
'''ἄγνος, -ου (nom commun) (m)''' : gattilier.<br>
'''ἁγνός, -ή, -όν (adjectif)''' : chaste ; pur.<br>
'''ἁγνότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἁγνός''.<br>
'''ἁγνότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἁγνός''.<br>
'''ἁγνότης, -τος (nom commun) (f)''' : chasteté ; pureté.<br>
'''ἁγνῶς (adverbe)''' : chastement ; purement.<br>
'''ἀγνοῶ (verbe)''' : ignorer.<br>
'''ἄγνωστος, -ος, -ον (adjectif)''' : (Sens passif) Inconnu, ignoré. (Sens actif) ignorant.<br>
'''ἀγνωστότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἄγνωστος''.<br>
'''ἀγνωστότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἄγνωστος''.<br>
'''ἀγνώστως (adverbe)''' : .<br>
'''ἀγορά, -ᾶς (nom commun) (f)''' : marché ; assemblée.<br>
'''ἀγορητής, -οῦ (nom commun) (m)''' : orateur.<br>
'''ἄγος, -ους (nom commun) (n)''' : Sacrilège, souillure. Homme sacrilège, impie. Expiation.<br>
'''ἀγριόχοιρος, -ίρου (nom commun) (m)''' : .<br>
'''ἀγροῖκος, -ίκα, -ῖκον (adjectif)''' : .<br>
'''ἄγριος, -ία, -ιον (adjectif)''' : qui vit dans les champs.<br>
'''ἀγρός, -οῦ (nom commun) (m)''' : (Au pluriel) Champ. (Au singulier) Ferme, bien de campagne, fonds, propriété foncière. La campagne (par opposition à la ville).<br>
'''ἄγρωστις, -ώστιδος (nom commun) (f)''' : chiendent officinal.<br>
'''ἄγυρις, -ύριος (nom commun) (f)''' : Forme éolienne de ''ἀγορά''.<br>
'''ἀγύρτης, -ου (nom commun) (m)''' : charlatan.<br>
'''ἀγών, -ῶνος (nom commun) (m)''' : Assemblée, réunion.<br>
'''ἀγωνία, -ας (nom commun) (f)''' : Lute dans les jeux, exercice, exercice gymnastique. (Figuré) Angoisse, anxiété.<br>
'''ἀγωνίζομαι (verbe)''' : Se battre, concourir pour un prix.<br>
'''ἀγωνιστής, -οῦ (nom commun) (m)''' : combattant.<br>
'''ἄγω (verbe)''' : Conduire, mener.<br>
'''ἀδαής, -ής, -ές (adjectif)''' : intrépide.<br>
'''ἀδάμας, -αντος (nom commun) (m)''' : diamant.<br>
'''ἀδελφή, -ῆς (nom commun) (f)''' : sœur.<br>
'''ἀδελφεά, -άς (nom commun) (f)''' : Forme dorienne de ''ἀδελφή''.<br>
'''ἀδελφεή, -ῆς (nom commun) (f)''' : Forme ionienne de ''ἀδελφή''.<br>
'''ἀδελφεός, -οῦ (nom commun) (m)''' : Forme homérique et ionienne de ''ἀδελφός''.<br>
'''ἀδελφειή, -ῆς (nom commun) (f)''' : Forme homérique de ''ἀδελφή''.<br>
'''ἀδελφιός, -οῦ (nom commun) (m)''' : Forme crétoise de ''ἀδελφός''.<br>
'''ἀδευφιός, -οῦ (nom commun) (m)''' : Autre forme crétoise de ''ἀδελφός''.<br>
'''ἀδελφός, -οῦ (nom commun) (m)''' : frère.<br>
'''ἀδεξιός, -ός, -όν (adjectif)''' : gauche ; maladroit.<br>
'''ἀδιάλλακτος, -ος, -ον (adjectif)''' : intransigeant.<br>
'''ἀδιανόητος, -ος, -ον (adjectif)''' : incompréhensible.<br>
'''ᾄδω (verbe)''' : Forme attique de ''ἀείδω''.<br>
'''ἀέ(ς) (adverbe)''' : Forme dorienne de ''ἀεί''.<br>
'''ἄεθλον, -ου (nom commun) (n)''' : Forme homérique de ''ἆθλον''.<br>
'''ἄεθλος, -ου (nom commun) (m)''' : Forme homérique et ionienne de ''ἆθλος''.<br>
'''ἀείδω (verbe)''' : chanter.<br>
'''ἀεικής, -ής, -ές (adjectif)''' : inapproprié ; malséant.<br>
'''ἀεικῶς (adverbe)''' : malséantement.<br>
'''ἀεικέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἀεικής''.<br>
'''ἀεικέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''ἀεικής''.<br>
'''ἀεί (adverbe)''' : toujours.<br>
'''ἀέλιος, -ίου (nom commun) (m)''' : Forme dorienne, éolienne, et arcado-chypriote de ''ἥλιος''.<br>
'''ἀεργός, -ή, -όν (adjectif)''' : .<br>
'''ἀετός, -οῦ (nom commun) (m)''' : aigle.<br>
'''ἄζα, -ης (nom commun) (f)''' : .<br>
'''ἀζαθός, -ή, -όν (adjectif)''' : Forme arcado-chypriote de ''ἀγαθός''.<br>
'''ἄζω (verbe)''' : .<br>
'''ἀηδών, -ονός (nom commun) (f)''' : rossignol.<br>
'''ἀήρ, -έρος (nom commun) (m)''' : air (que l'on respire).<br>
'''ἄθεος, -ος, -ον (adjectif)''' : athée.<br>
'''ἀθίγγανος, -ου (nom commun) (m)''' : tsigane.<br>
'''ἄθικτος, -ος, -ον (adjectif)''' : intact.<br>
'''ἄθλημα, -ήματος (nom commun) (n)''' : sport.<br>
'''ἀθλητής, -οῦ (nom commun) (m)''' : sportif.<br>
'''ἀθλητικός, -ή, -όν (adjectif)''' : sportif.<br>
'''ἀθλητικῶς (adverbe)''' : sportivement.<br>
'''ἀθλητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀθλητικός''.<br>
'''ἀθλητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀθλητικός''.<br>
'''ἀθλητικώτατα, -, - (adverbe)''' : Superlatif de ''ἀθλητικῶς''.<br>
'''ἀθλητικώτερον, -, - (adverbe)''' : Comparatif de ''ἀθλητικῶς''.<br>
'''ἄθλιος, -ία, -ιον (adjectif)''' : Concourant pour un prix ou luttant pour gagner un prix. Misérable, fieffé ; désolé.<br>
'''ἀθλίως (adverbe)''' : misérablement.<br>
'''ἄθλιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἄθλιος''.<br>
'''ἄθλιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἄθλιος''.<br>
'''ἆθλον, ἄθλου (nom commun) (n)''' : .<br>
'''ἆθλος, ἄθλου (nom commun) (m)''' : compétition.<br>
'''ἄθυρμα, -ύρματος (nom commun) (n)''' : jouet.<br>
'''ἀθύρω (verbe)''' : jouer.<br>
'''ἀθῷος, -ος, -ον (adjectif)''' : innocent.<br>
'''ἀθῷότης, -τος (nom commun) (f)''' : innocence.<br>
'''αἴ (interjection)''' : hélas.<br>
'''αἰϐοῖ (interjection)''' : beurk.<br>
'''αἰγαῖος, -ία, -ῖον (adjectif)''' : égéen.<br>
'''αἰγίθαλλος, -άλλου (nom commun) (m)''' : mésange.<br>
'''αἰγίς, -δος (nom commun) (f)''' : égide.<br>
'''αἴγλη, -ης (nom commun) (f)''' : splendeur.<br>
'''αἰδοΐα, -ας (nom commun) (f)''' : organe génital.<br>
'''αἰδοῖος, -ία, -ῖον (adjectif)''' : pudique.<br>
'''αἰδοῖον, -ίου (nom commun) (n)''' : vulve.<br>
'''αἰδώς, -οῦς (nom commun) (f)''' : pudeur.<br>
'''αἰεί (adverbe)''' : Forme ionienne de ''ἀεί''.<br>
'''αἰέν (adverbe)''' : Forme homérique de ''ἀεί''.<br>
'''αἰές (adverbe)''' : Autre forme dorienne de ''ἀεί''.<br>
'''αἰή (adverbe)''' : Autre forme dorienne de ''ἀεί''.<br>
'''αἰθάλη, -ης (nom commun) (f)''' : .<br>
'''αἴθαλος, -άλου (nom commun) (m)''' suie.<br>
'''αἰθήρ, -έρος (nom commun) (m/f)''' : éther.<br>
'''αἰθιοπικός, -ή, -όν (f)''' : éthiopien.<br>
'''αἶθος, -ἴθους (nom commun) (n)''' : Chaleur, feu.<br>
'''αἴθουσα, - (nom commun) (f)''' : .<br>
'''αἴθριος, -ια, -ιον (adjectif)''' : Clair, limpide ; beau.<br>
'''αἰθύλιον, -ίου (nom commun) (n)''' : éthyle.<br>
'''αἴθω (verbe)''' : brûler.<br>
'''αἶι (adverbe)''' : Forme éolienne de ''ἀεί''.<br>
'''-ακός, -ή, -όν (suffixe)''' : .<br>
'''αἴξ, -γός (nom commun) (f)''' : chèvre.<br>
'''αἰολικός, -ή, -όν (adjectif)''' : éolien.<br>
'''αἰόλος, -α, -ον (adjectif)''' : agité.<br>
'''αἰλουρίς, -δος (nom commun) (f)''' : chatte.<br>
'''αἴλουρος, -ύρου (m/f)''' : Chat, chatte.<br>
'''αἷμα, -ἵματος (nom commun) (n)''' : sang.<br>
'''αἱμωδία, -ας (nom commun) (f)''' : hémodie.<br>
'''αἱμωδιῶ (verbe)''' : saigner des dents.<br>
'''αἱμοπτύσις, -εως (nom commun) (f)''' : crachement de sang.<br>
'''αἱμορραγία, -ας (nom commun) (f)''' : perte de sang.<br>
'''αἱμορραγῶ (verbe)''' : perdre du sang.<br>
'''αἱμορροΐς, -δος (nom commun) (f)''' : hémorroïde.<br>
'''αἱμορροῶ (verbe)''' : saigner.<br>
'''αἱμόφυρτος, -ος, -ον (adjectif)''' : sanglant.<br>
'''-αινα, -ίνης (suffixe) (f)''' : .<br>
'''αἴνεσις, -έσεως (nom commun) (f)''' : louange.<br>
'''αἰνέω (verbe)''' : louer.<br>
'''αἴνιγμα, -ίγματος (nom commun) (n)''' : puzzle.<br>
'''αἰνιγματικός, -ή, -όν (adjectif)''' : relatif aux puzzles.<br>
'''αἰνίσσομαι (verbe)''' : parler par mystères.<br>
'''αἶνος, -ἵνου (nom commun) (m)''' : fable.<br>
'''αἰνῶ (verbe)''' : Parler de (suivi de l’accusatif). Trouver bon.<br>
'''-αῖος, -ία -ῖον (suffixe)''' : ancien.<br>
'''-αιότατος (suffixe)''' : Forme superlative de ''-αῖος''.<br>
'''-αιότερος (suffixe)''' : Forme comparative de ''-αῖος''.<br>
'''-αίως (suffixe)''' : Forme adverbiale de ''-αῖος''.<br>
'''αἰπόλος, -ου (nom commun) (m)''' : chevrier.<br>
'''αἶρα, -ἴρας (nom commun) (f)''' : action de prendre, prise ; choix.<br>
'''αἵρεσις, -έσεως (nom commun) (f)''' : action de prendre, prise ; choix.<br>
'''αἱρῶ (verbe)''' : Forme ionienne et poétique de ''αἴρω''.<br>
'''αἴρω (verbe)''' : Lever. (Par suite) Enlever, supprimer, détruire, faire périr. (Par extension) Contester, nier. (Figuré) Faire une levée. (Figuré) Élever, exalter, grandir. Mettre hors de soi. (Passif) Être transporté.<br>
'''αἶσα, -ἴσης (nom commun) (f)''' : Part ; destinée.<br>
'''αἴσθημα, -ήματος (nom commun) (n)''' : sentiment.<br>
'''αἴσθησις, -ήσεως (nom commun) (f)''' : Faculté de percevoir les sens, sensation. (Par extension) Action de percevoir l’intelligence, action de s’apercevoir. Organe des sens. L’un des cinq sens. Piste d’un animal.<br>
'''αἰσθητήριος, -ος, -ον (adjectif)''' : sensoriel.<br>
'''αἴσακος, -άκου (nom commun) (m)''' : .<br>
'''αἰσιοδοξία, -ας (nom commun) (f)''' : optimisme.<br>
'''αἶσχος, -ἴσχους (nom commun) (n)''' : honte.<br>
'''αἰσχρολογία, -ας (nom commun) (f)''' : obscénité.<br>
'''αἰσχρός, -ά, -όν (adjectif)''' : laid, honteux.<br>
'''αἰσχρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''αἰσχρός''.<br>
'''αἰσχρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''αἰσχρός''.<br>
'''αἰσχρῶς (adverbe)''' : honteusement.<br>
'''αἰσχύνη, -ης (nom commun) (f)''' : opprobre ; vergogne.<br>
'''αἰσχύνω (verbe)''' : déshonorer.<br>
'''αἰτέω (verbe)''' : Demander. (Dialogue) Poser un postulat.<br>
'''αἴτησις, -ήσεως (nom commun) (f)''' : demande.<br>
'''αἰχμαλωσία, -ας (nom commun) (f)''' : captivité.<br>
'''αἰχμαλωτίζω (verbe)''' : attraper ; capturer, saisir.<br>
'''αἰχμάλωτος, - (nom commun) (m)''' : captif.<br>
'''αἰών, -ῶνος (nom commun) (m)''' : temps (durée de la vie) ; moelle épinière.<br>
'''αἰώνιος, -ος, -ον (adjectif)''' : Éternel. Perpétuel. Séculaire.<br>
'''ἀΐω (verbe)''' : entendre.<br>
'''ἀκαδημία, -ας (nom commun) (f)''' : académie.<br>
'''ἀκακία, -ας (nom commun) (f)''' : innocence.<br>
'''ἄκανθα, -ης (nom commun) (f)''' : épine, piquant.<br>
'''ἀκανθοκνίδη, -ης (nom commun) (f)''' : ortie.<br>
'''ἀκανθόχοιρος, -ίρου (nom commun) (m)''' : porc-épic.<br>
'''ἀκαρής, -ής, -ές (adjectif)''' : insécable.<br>
'''ἄκαρι, -άρεως (nom commun) (n)''' : ciron ; mite.<br>
'''ἀκαριαῖος, -ία, -ῖον (adjectif)''' : instantané.<br>
'''ἀκίνδυνος, -η, -ον (adjectif)''' : inoffensif ; sûr.<br>
'''ἀκκίζομαι (verbe)''' : .<br>
'''ἀκκιστικός, -ή, -όν (adjectif)''' : .<br>
'''ἀκκισμός, -οῦ (nom commun) (m)''' : .<br>
'''ἄκμων, -ονος (nom commun) (m)''' : enclume.<br>
'''ἀκοίτης, -ου (nom commun) (m)''' : époux.<br>
'''ἄκοιτις, -ίτης (nom commun) (f)''' : épouse.<br>
'''ἀκοή, -ῆς (nom commun) (f)''' : ouïe.<br>
'''ἀκολασία, -ας (nom commun) (f)''' : débauche, luxure ; stupre.<br>
'''ἀκολουθία, -ας (nom commun) (f)''' : escorte ; suite.<br>
'''ἀκόλουθος, -ύθου (nom commun) (m)''' : domestique ; serviteur.<br>
'''ἀκολούθως (adverbe)''' : après ; ensuite.<br>
'''ἀκολουθῶ (verbe)''' : escorter ; suivre.<br>
'''ἀκόρεστος, -ος, -ον (adjectif)''' : (passif) Insatiable, inépuisable. (actif) Qui ne cause aucune satiété.<br>
'''ἀκουστικός, -ή, -όν (adjectif)''' : relatif à l'ouïe.<br>
'''ἀκούω (verbe)''' : écouter, entendre.<br>
'''ἀκρίς, -δος (nom commun) (f)''' : Sauterelle ; criquet.<br>
'''ἀκροϐάτης, -ου (nom commun) (m)''' : Danseur de corde, faiseur de tours d’agilité.<br>
'''ἀκροποσθία, -ας (nom commun) (f)''' : prépuce.<br>
'''ἀκρόπολις, -όλεως (nom commun) (f)''' : citadelle.<br>
'''ἀκροστιχίς, -δος (nom commun) (f)''' : acrostiche.<br>
'''ἄκρος, -α, -ον (adjectif)''' : extrême.<br>
'''ἄκρον -ου (nom commun) (n)''' : .<br>
'''ἀκτίς, -ῖνος (nom commun) (f)''' : rayon.<br>
'''ἀκύρωσις, -ώσεως (nom commun) (f)''' : .<br>
'''ἀκυρῶ (verbe)''' : .<br>
'''ἀλαζονεία, -ας (nom commun) (f)''' : .<br>
'''ἀλαζόνευμα, -ύματος (nom commun) (n)''' : .<br>
'''ἀλαζονεύομαι (verbe)''' : .<br>
'''ἀλαζονικός, -ή, -όν (adjectif)''' : .<br>
'''ἀλαζονίστατα (nom commun) (f)''' : .<br>
'''ἀλαζών, -όνος (nom commun) (m/f)''' : Imposteur, sans domicile fixe, escroc. Vantard.<br>
'''ἀλαζών, -ών, -όν (adjectif)''' : .<br>
'''ἀλάομαι (verbe)''' : vagabonder.<br>
'''ἀλάϐαστρος, -άστρου (nom commun) (m)''' : vase de plâtre.<br>
'''ἀλατιστός, -ή, -όν (adjectif)''' : .<br>
'''ἄλγος, -ους (nom commun) (n)''' : Souffrance. Douleur physique. Peine, affliction. Sujet de peine.<br>
'''ἀλείτης, -ου (nom commun) (m)''' : .<br>
'''ἄλειμμα, -ίμματος (nom commun) (n)''' : .<br>
'''ἀλειπτήριον, -ίου (nom commun) (m)''' : .<br>
'''ἀλειπτήρ, -ῆρος (nom commun) (m)''' : .<br>
'''ἀλείπτης ''' : .<br>
'''ἀλειπτός ''' : .<br>
'''ἄλειψις, -ῆρος (nom commun) (m)''' : .<br>
'''ἄλειφαρ, -ίφατος (nom commun) (n)''' : pommade.<br>
'''ἀλείφω (m)''' : pommader.<br>
'''ἀλεκτρυών, -όνος (nom commun) (m/f)''' : Coq ; poule.<br>
'''ἀλέκτωρ, -ορος (nom commun) (m)''' : coq.<br>
'''ἀλεξανδρῖνος, -ίνη, -ῖνον (adjectif)''' : alexandrin.<br>
'''ἀλέξω (verbe)''' : défendre. (prendre la défense)<br>
'''ἄλευρον, -ύρου (nom commun) (n)''' : farine de froment.<br>
'''ἀλευρώδης, -ώδης, -ῶδες (n)''' : semblable à de la farine de froment.<br>
'''ἀλέω (verbe)''' : moudre.<br>
'''ἄλη, -ης (nom commun)''' : errance.<br>
'''ἀλήθεια, -ίας (nom commun) (f)''' : vérité.<br>
'''ἀληθέστατος, -άτη, -έστατον (adjectif) (adjectif)''' : Superlatif de ''ἀληθής''.<br>
'''ἀληθέστερος, -έρα, -έτερον (adjectif)''' : Comparatif de ''ἀληθής''.<br>
'''ἀληθής, -ής, -ές (adjectif)''' : vrai.<br>
'''ἀληθινός, -ή, -όν (adjectif)''' : vrai.<br>
'''ἀληθινώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀληθινός''.<br>
'''ἀληθινώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀληθινός''.<br>
'''ἀληθινώτατα, -, - (adverbe)''' : Superlatif de ''ἀληθινῶς''.<br>
'''ἀληθινώτερον, -, - (adverbe)''' : Comparatif de ''ἀληθινῶς''.<br>
'''ἀληθινῶς (adverbe)''' : vraiment.<br>
'''ἀληθῶς (adverbe)''' : vraiment.<br>
'''ἀλήτης, -ου (nom commun) (m)''' : .<br>
'''ἁλιεύς, -έως (nom commun) (m)''' : pêcheur.<br>
'''ἁλιευτικός, -ή, -όν (adjectif)''' : qui concerne la pêche.<br>
'''ἁλιεύω (verbe)''' : pêcher.<br>
'''ἅλιος, -ίου (nom commun) (m)''' : Forme de ''ἥλιος''.<br>
'''ἀλιτρός, -ός, -όν (adjectif)''' : vilain.<br>
'''ἀλιτρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἀλιτρός''.<br>
'''ἀλιτρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἀλιτρός''.<br>
'''ἀλιτρῶς (adverbe)''' : vilainement.<br>
'''ἀλκή, -ῆς (nom commun) (f)''' : force.<br>
'''ἀλκμαῖος, -ία, -ῖον (adjectif)''' : jeune.<br>
'''ἀλλά (conjonction)''' : (Devient ''ἀλλ’'' devant un mot commençant par une voyelle.) Mais. D’un autre côté, autrement.<br>
'''ἀλλαγή, -ῆς (nom commun) (f)''' : Changement ; échange.<br>
'''ἀλλάσσω (verbe)''' : Changer, altérer. Échanger.<br>
'''ἄλλαξις, -άξεως (nom commun) (f)''' : troc.<br>
'''ἄλληλος (adjectif)''' : .<br>
'''ἀλληλούϊα (interjection)''' : alléluia.<br>
'''ἁλμάω (verbe)''' : saumurer.<br>
'''ἅλμη, -ης (nom commun) (f)''' : saumure.<br>
'''ἀλόη, -ης (nom commun) (f)''' : aloès.<br>
'''ἀλοιφή, -ῆς (nom commun) (f)''' : pommade.<br>
'''ἇλος, ἅλου (nom commun) (m)''' : Forme dorienne de ''ἧλος''.<br>
'''ἄλσος, -ους (nom commun) (n)''' : bois (lieu).<br>
'''ἅλς, -ός (nom commun) (m/f)''' : sel ; mer.<br>
'''ἅλυσις, -ύσεως (nom commun) (f)''' : chaîne (succession d’anneaux enserrés).<br>
'''ἄλυσις, -ύσεως (nom commun) (f)''' : détresse ; angoisse.<br>
'''ἄλυσσον, -ύσσου (nom commun) (n)''' : alysse.<br>
'''ἀλύω (verbe)''' : Être tout excité ; divaguer.<br>
'''ἄλφα (nom commun) (n)''' : alpha.<br>
'''ἀλφάϐητος, -ήτου (nom commun) (m)''' : Ensemble des lettres servant à écrire.<br>
'''ἄλφιτα, -ίτας (nom commun) (f)''' : farine d’orge.<br>
'''ἀλφός, -οῦ (nom commun) (m)''' : lèpre blanche.<br>
'''ἀλωπεκῆ, -ῆς (nom commun) (f)''' : peau de renard ; ruse.<br>
'''ἀλωπεκία, -ας (nom commun) (f)''' : chute des cheveux.<br>
'''ἀλωπεκίασις, -άσεως (nom commun) (f)''' : chute des cheveux.<br>
'''ἀλωπεκίς, -δος (nom commun) (f)''' : casquette en peau de renard.<br>
'''ἀλώπηξ, -εκος (nom commun) (f)''' : renard.<br>
'''ἅμμα, -τος (nom commun) (n)''' : nœud.<br>
'''ἅμαξα, -ης (nom commun) (f)''' : chariot.<br>
'''ἁμαξαία, -ας (nom commun) (f)''' : .<br>
'''ἁμαξαῖος, -ία, -ῖον (adjectif)''' : .<br>
'''ἁμαξακάρινον, -ίνου (nom commun) (n)''' : .<br>
'''ἁμαξάρχης, -ου (nom commun) (m)''' : .<br>
'''ἀμαυρός, -ά, -όν (adjectif)''' : sombre ; obscur.<br>
'''ἀμαύρωσις, -ώσεως (nom commun) (f)''' : obscurcissement.<br>
'''ἀμαυρῶ (verbe)''' : s’obscurcir.<br>
'''ἁμαρτάς, -δος (nom commun) (f)''' : Faute ; erreur, méprise.<br>
'''ἁμαρτάνω (verbe)''' : Manquer le but. Se tromper, se méprendre. Commettre une faute, faillir, pécher.<br>
'''ἁμάρτημα, -ήματος (nom commun) (n)''' : Échec ; faute. Péché.<br>
'''ἁμαρτία, -ας (nom commun) (f)''' : Erreur ; faute. Péché<br>
'''ἀμάτωρ, -ωρ, -ορ (adjectif)''' : Forme dorienne de ''ἀμήτωρ''.<br>
'''ἀμϐλύς, -εῖα, -ύ (adjectif)''' : émoussé.<br>
'''ἀμϐρόσιος, -ία, -όσιον (adjectif)''' : divin ; immortel.<br>
'''ἀμϐροσίως (adverbe)''' : divinement ; immortellement.<br>
'''ἀμϐροσιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀ μϐρόσιος''.<br>
'''ἀμϐροσιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀμϐρόσιος''.<br>
'''ἄμϐων, -ος (nom commun) (m)''' : Bord arrondi d’un vase.<br>
'''ἀμέθυστος, -ος, -ον (adjectif)''' : sobre.<br>
'''ἀμεθύω (verbe)''' : être sobre.<br>
'''ἀμέλγω (verbe)''' : Traire. Sucer, boire.<br>
'''ἀμελής, -ής, -ής (adjectif)''' : négligeant.<br>
'''ἀμελῶ (verbe)''' : négliger.<br>
'''ἀμέρα, -ας (nom commun) (f)''' : Forme dorienne de ''ἡμέρα''.<br>
'''ἀμήν (adverbe)''' : amen.<br>
'''ἀμήτωρ, -ωρ, -ορ (adjectif)''' : sans mère.<br>
'''ἀμίς, -δος (nom commun) (f)''' : pot de chambre.<br>
'''ἀμνάς, -δος (nom commun) (f)''' : agnelle.<br>
'''ἀμνός, -οῦ (nom commun) (m)''' : agneau.<br>
'''ἄμεσος (adverbe)''' : .<br>
'''ἀμπέλιον, -ίoυ (nom commun) (n)''' : petite vigne.<br>
'''ἄμπελος, -έλου (nom commun) (m)''' : vigne (plante).<br>
'''ἀμπελών, -ος (nom commun) (m)''' : vigne (endroit où elle est plantée).<br>
'''ἀμπρακικός, -η, -όν (adjectif)''' : ambracien.<br>
'''ἀμυγδάλη, -ης (nom commun) (f)''' : amygdale.<br>
'''ἀμύγδαλον, -άλoυ (nom commun) (n)''' : amande. (fruit)<br>
'''ἄμυλος, -ύλoυ (nom commun) (m)''' : amidon.<br>
'''ἀμύνω (verbe)''' : défendre.<br>
'''ἀμφί (adverbe ; préposition)''' : Autour. Séparément ; pour soi. Autour. À partir de ; loin de. (Avec le génitif)
Autour de ; au milieu de. (Avec le datif) Autour de (quelque chose). (Figuré) Au sujet de ; par suite de. (Avec l’accusatif) Autour de. (Par extension) En faisant le tour de : en circulant à travers ; à travers ; par. Au sujet de. Aux environs de. (Joint à ''περί'') Tout autour de.<br>
'''ἀμφί- (préfixe)''' : De deux côtés ; en double. Tout autour de. Au sujet de.<br>
'''ἀμφιθέατρον, -ου (nom commun) (n)''' : amphithéâtre.<br>
'''ἀμφιθέητρον, -ου (nom commun) (n)''' : Forme ionienne de ''ἀμφιθέατρον''.<br>
'''ἀμφισϐητήσιμος, -η, -ον (adjectif)''' : contestable.<br>
'''ἀμφισϐητῶ (n)''' : contester.<br>
'''ἀμφί (adverbe ; préposition)''' : Des deux côtés ; aux deux extrémités. Autour. À partir de ; loin de.<br>
'''ἀμφίς (adverbe ; préposition)''' : Des deux côtés ; aux deux extrémités. Autour. À partir de ; loin de.<br>
'''ἀμφορεύς, -έως (nom commun) (m)''' : amphore.<br>
'''ἀμφότερος, -έρα, -ότερον (adjectif)''' : l’un l’autre, les deux.<br>
'''ἄμφω (déterminant)''' : les deux.<br>
'''ἀνά (adverbe, préposition)''' : En haut. En avant. En haut de, sur, à travers.<br>
'''ἄνα- (préfixe)''' : De bas en haut. En arrière. (Par suite) faire le contraire. (Par extension) Faire de nouveau.<br>
'''ἀναϐάλλω (verbe)''' : .<br>
'''ἀναϐολεύς, -έως (nom commun) (m)''' : étrier.<br>
'''ἀναϐολή, -ῆς (nom commun) (f)''' : ajournement, sursis.<br>
'''ἀναγκάζω (verbe)''' : forcer, contraindre.<br>
'''ἀναγκαῖος, -ία, -ῖον (adjectif)''' : (actif) Qui contraint. Nécessaire. Parent pour le sang. (passif) Contraint, forcé.<br>
'''ἀνάγκη, -ης (nom propre) (f)''' : Nécessité, contrainte.<br>
'''ἀναγράφω (verbe)''' : .<br>
'''ἀνάδημα, -τος (nom commun) (n)''' : anadème.<br>
'''ἀνάδοχος, -όχου (nom commun) (m/f)''' : parrain, marraine.<br>
'''ἀναδρομή, -ῆς (nom commun) (f)''' : rétrospective.<br>
'''ἀναδρομικός, -ή, -όν (adjectif)''' : rétrospectif.<br>
'''ἀναδρομικῶς (adverbe)''' : rétrospectivement.<br>
'''ἀνάδυσις, -ύσεως (nom commun) (f)''' : émergence.<br>
'''ἀναδύομαι (verbe)''' : émerger.<br>
'''ἀναζήτησις, -ήσεως (nom commun) (f)''' : recherche.<br>
'''ἀναζητῶ (verbe)''' : rechercher.<br>
'''ἀναθεωρῶ (verbe)''' : réviser.<br>
'''ἀναθυμίασις, -άσεως (nom commun) (f)''' : effluve.<br>
'''ἀναίδεια, -ας (nom commun) (f)''' : impudence.<br>
'''ἀναιδέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἀναιδής''.<br>
'''ἀναιδέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''ἀναιδής''.<br>
'''ἀναιδής, -ής, -ές (adjectif)''' : impudent.<br>
'''ἀναιδῶς (adverbe)''' : impudemment.<br>
'''ἀναιμία, -ας (nom commun) (f)''' : manque de sang.<br>
'''ἀναισχυντία, -ας (nom commun) (f)''' : effronterie.<br>
'''ἀναίσχυντος, -ή, -όν (adjectif)''' : effronté.<br>
'''ἀναίσχυντότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἀναίσχυντός''.<br>
'''ἀναίσχυντότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἀναίσχυντός''.<br>
'''ἀναίσχυντότατα, -, - (adverbe)''' : Superlatif de ''ἀναίσχυντῶς''.<br>
'''ἀναίσχυντότερον, -, - (adverbe)''' : Comparatif de ''ἀναίσχυντῶς''.<br>
'''ἀναίσχυντῶς (adverbe)''' : effrontément.<br>
'''ἀνακινῶ (verbe)''' : .<br>
'''ἀνακοίνωσις, -ώσεως (nom commun) (f)''' : communication.<br>
'''ἀνακοινῶ (verbe)''' : communiquer.<br>
'''ἀνακουφίζω (verbe)''' : soulager.<br>
'''ἀνακούφισις, -ίσεως (nom commun) (f)''' : soulagement.<br>
'''ἀνακτορία, -ας (nom commun) (f)''' : .<br>
'''ἀνακτόριος, -ος, -ον (adjectif)''' : .<br>
'''ἀνάκτορον, -όρου (nom commun) (n)''' : .<br>
'''ἀναλαμϐάνω (verbe)''' : reprendre.<br>
'''ἀνάλαψις, -άψεως (nom commun) (f)''' : Forme dorienne de ''ἀνάληψις''.<br>
'''ἀνάληψις, -ήψεως (nom commun) (f)''' : Reprise, reprise de forces, rétablissement. Reconnaissance d'un enfant, action de le faire sien. (Religion chrétienne) Ascension, action d’être repris par le Ciel. Réception.<br>
'''ἀναλογία, -ας (nom commun) (f)''' : proportion.<br>
'''ἀναλογικός, -ή, -όν (adjectif)''' : proportionnel.<br>
'''ἀναλογικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἀναλογικός''.<br>
'''ἀναλογικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἀναλογικός''.<br>
'''ἀναλογικότατα, -, - (adverbe)''' : Superlatif de ''ἀναλογικῶς''.<br>
'''ἀναλογικότερον, -, - (adverbe)''' : Comparatif de ''ἀναλογικῶς''.<br>
'''ἀναλογικῶς (adverbe)''' : proportionnellement.<br>
'''ἀνάλογος, -η, -ον (adjectif)''' : commensurable.<br>
'''ἀναμένω (verbe)''' : surveiller.<br>
'''ἀνάμνησις, -ήσεως (nom commun) (f)''' : commémoration.<br>
'''ἀναμνηστικός, -ή, -όν (adjectif)''' : commémoratif.<br>
'''ἀναμνηστικῶς (adverbe)''' : commémorativement.<br>
'''ἀναμνηστικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀναμνηστικός''.<br>
'''ἀναμνηστικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀναμνηστικός''.<br>
'''ἀναμνηστικώτατα, -, - (adverbe)''' : Superlatif de ''ἀναμνηστικῶς''.<br>
'''ἀναμνηστικώτερον, -, - (adverbe)''' : Comparatif de ''ἀναμνηστικῶς''.<br>
'''ἄναξις, -εως (nom commun) (f)''' : .<br>
'''ἄναξ, -κτος (nom commun) (m)''' : maître, chef, roi.<br>
'''ἀνάπαιστος, -ίστου (nom commun) (m)''' : anapeste.<br>
'''ἀνάπαυσις, -ύσεως (nom commun) (f)''' : repos.<br>
'''ἀναπαύω (verbe)''' : reposer.<br>
'''ἄνασσα, -ας (nom commun) (f)''' : maîtresse, reine.<br>
'''ἀνάστασις, -άσεως (nom commun) (f)''' : résurrection.<br>
'''ἀναστεναγμός, -οῦ (nom commun) (m)''' : soupir.<br>
'''ἀναστενάζω (verbe)''' : soupirer.<br>
'''ἀνατέλλω (verbe)''' : .<br>
'''ἀνατολή, -ῆς (nom commun) (f)''' : est.<br>
'''ἀνατολικός, -ή, -όν (adjectif)''' : oriental.<br>
'''ἀνατολικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἀνατολικός''.<br>
'''ἀνατολικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἀνατολικός''.<br>
'''ἀνατολικότατα, -, - (adverbe)''' : Superlatif de ''ἀνατολικῶς''.<br>
'''ἀνατολικότερον, -, - (adverbe)''' : Comparatif de ''ἀνατολικῶς''.<br>
'''ἀνατολικῶς (adverbe)''' : orientalement.<br>
'''ἀναφέρω (verbe)''' : alléguer ; mentionner.<br>
'''ἀναφλέγω (verbe)''' : enflammer.<br>
'''ἀνάφλεξις, -έξεως (nom commun) (f)''' : combustion.<br>
'''ἀναφορά, -ᾶς (nom commun) (f)''' : allégation, mention.<br>
'''ἀναφορικός, -ή, -όν (adjectif)''' : allégatoire.<br>
'''ἀναχώρησις, -ήσεως (nom commun) (f)''' : départ.<br>
'''ἀνδραποδίζω (verbe)''' : vendre des hommes libres en esclavage.<br>
'''ἀνδράποδον, -όδου (nom commun) (n)''' : captif.<br>
'''ἀνδρείκελον, -έλου (nom commun) (n)''' : marionnette.<br>
'''ἀνδρεῖος, -ία, -ῖον (adjectif)''' : masculin.<br>
'''ἀνδρειότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἀνδρεῖος''.<br>
'''ἀνδρειότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἀνδρεῖος''.<br>
'''ἀνδρείως (adverbe)''' : masculinement.<br>
'''ἀνδρομανία, -ας (nom commun) (f)''' : andromanie.<br>
'''ἀνδροπρέπεια, -ίας (nom commun) (f)''' : virilité.<br>
'''ἀνδροπρεπής -ής -ές (adjectif)''' : viril.<br>
'''ἀνδροπρεπῶς (adverbe)''' : virilement.<br>
'''ἀνδρότης, -τος (nom commun) (f)''' : virilité.<br>
'''ἀνδρών, -ῶνος (nom commun) (m)''' : Appartement réservé aux hommes.<br>
'''ἀνεκδιήγητος, -ος, -ον (adjectif)''' : inénarrable.<br>
'''ἀνέκδοτος, -ος, -ον (adjectif)''' : Non donnée en mariage, en parlant d’une jeune fille. Inédit, non publié.<br>
'''ἀνέμη, -ης (nom commun) (f)''' : rouet.<br>
'''ἄνεμος, -έμου (nom commun) (m)''' : vent.<br>
'''ἀνερρίπτω (verbe)''' : .<br>
'''ἀνερωτῶ (verbe)''' : .<br>
'''ἀνευθυνότης, -ητος (nom commun) (f)''' : irresponsabilité.<br>
'''ἀνευθύνω (verbe)''' : être irresponsable.<br>
'''ἀνεύρυσμα, -ύσματος (nom commun) (n)''' : élargissement, dilatation.<br>
'''ἀνευρύνω (verbe)''' : élargir, dilater.<br>
'''ἀνεψιά, -ᾶς (nom commun) (f)''' : nièce.<br>
'''ἀνεψιός, -οῦ (nom commun) (m)''' : neveu.<br>
'''ἄνηθον, -ήθου (nom commun) (n)''' : aneth.<br>
'''ἀνήρ, -δρός (nom commun) (n)''' : Homme, époux ; mâle des animaux.<br>
'''ἄνθεμον, -έμου (nom commun) (n)''' : Diminutif d’''ἄνθος''.<br>
'''ἀνθολογέω (verbe)''' : cueillir des fleurs.<br>
'''ἁνθολογία, -ας (nom commun) (f)''' : florilège.<br>
'''ἀνθολόγιον, -ου (nom commun) (n)''' : recueil.<br>
'''ἀνθόλωψ, -πος (nom commun) (m)''' : antilope.<br>
'''ἄνθος, -ους (nom commun) (n)''' : fleur.<br>
'''ἄνθραξ, κος (nom commun) (m)''' : charbon.<br>
'''ἀνθρακιά, -ᾶς (nom commun) (f)''' : pile de charbon.<br>
'''ἀνθρωπάριον, -ίου (nom commun) (n)''' : homuncule.<br>
'''ἀνθρώπινος, -η, -ον (adjectif)''' : humain.<br>
'''ἀνθρωπίσκος, -ου (nom commun) (m)''' : mannequin.<br>
'''ἄνθρωπος, -ώπου (nom commun) (m)''' : homme, genre humain.<br>
'''ἀνθρωποκεντρικός, -ή, -όν (adjectif)''' : anthropocentrique.<br>
'''ἀνθρωποειδής, -ής, -ες, (adjectif)''' : anthropoïde.<br>
'''ἀνθρωπολογία, -ας''' : anthropologie.<br>
'''ἀνθρωπομορφισμός, -οῦ (nom commun) (m)''' : anthropomorphisme.<br>
'''ἀνθρωποφαγία, -ας (nom commun) (f)''' : cannibalisme.<br>
'''ἀνθρωποφάγος, -ου (nom commun) (n)''' : cannibale.<br>
'''ἀνία, -ας (nom commun) (f)''' : ennui.<br>
'''ἁνία, -ας (nom commun) (f)''' : Forme dorienne de ''ἡνία''.<br>
'''ἄνισος, -η, -ον (adjectif)''' : inégal.<br>
'''ἀνίστημι (verbe)''' : ressusciter.<br>
'''ἄν (adverbe, particule)''' : .<br>
'''ἄννησον, -ήσου (nom commun) (n)''' : anis.<br>
'''ἀνόδων, -οντος (nom commun) (m/f)''' : édenté.<br>
'''ἀνόητος, -ος, -ον (adjectif)''' : .<br>
'''ἄνοια, -ας (nom commun) (f)''' : démence.<br>
'''ἄνοιγμα, -ίγματος (nom commun) (n)''' : orifice, ouverture.<br>
'''ἀνοίγω (verbe)''' : ouvrir.<br>
'''ἄνοιξις, -ίξεως (nom commun) (f)''' : ouverture.<br>
'''ἀνωμαλία, -ας (nom commun) (f)''' : inégalité, irrégularité.<br>
'''ἀνώμαλος, -ος, -ον (adjectif)''' : inégal, irrégulier.<br>
'''ἄνοικτος, -ος, -ον (adjectif)''' : impitoyable.<br>
'''ἀνοικτότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἄνοικτος''.<br>
'''ἀνοικτότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἄνοικτος''.<br>
'''ἀνοικτότατα, -, - (adverbe)''' : Superlatif de ''ἀνοίκτως''.<br>
'''ἀνοικτότερον, -, - (adverbe)''' : Comparatif de ''ἀνοίκτως''.<br>
'''ἀνοίκτως (adverbe)''' : impitoyablement.<br>
'''ἀνορεξία, -ας (nom commun) (f)''' : manque d’appétit.<br>
'''ἄνους, -ους, -ουν (adjectif)''' : dément.<br>
'''ἀνταγωνίζεσθαι (verbe)''' : contrarier.<br>
'''ἀνταγωνιστής, -οῦ (nom commun) (m)''' : adversaire.<br>
'''ἀνταίρω (verbe)''' : se rebeller.<br>
'''ἀνταπαίτησις, -ήσεως (nom commun) (f)''' : .<br>
'''ἀνταπαιτητής, -οῦ (nom commun) (m)''' : .<br>
'''ἀνταπαιτῶ (verbe)''' : .<br>
'''ἀνταρσία, -ας (nom commun) (m)''' : rébellion.<br>
'''ἀντάρτης, -ου (nom commun) (m)''' : rebelle.<br>
'''ἀντάρτικος, -η, -ον (adjectif)''' : rebelle.<br>
'''ἀντάρτικῶς (adverbe)''' : Forme adverbiale de ''ἀντάρτικός''.<br>
'''ἀντάρτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀντάρτικός''.<br>
'''ἀντάρτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀντάρτικός''.<br>
'''ἀντάρτικώτατα, -, - (adverbe)''' : Superlatif de ''ἀντάρτικῶς''.<br>
'''ἀντάρτικώτερον, -, - (adverbe)''' : Comparatif de ''ἀντάρτικῶς''.<br>
'''ἀντέχω (verbe)''' : endurer.<br>
'''ἀντιάς, -δος (nom commun) (f)''' : (anatomie) amygdale.<br>
'''ἀντιγραφέυς, -έως (nom commun) (m)''' : copiste.<br>
'''ἀντιγραφή, -ῆς (nom commun) (f)''' : recopiage.<br>
'''ἀντιγράφω (verbe)''' : copier. (un texte écrit)<br>
'''ἀντί (préposition)''' : En face de. À l’encontre de, contre. Au lieu de, à la place de. À l’égal de. En échange de.
Par succession, par addition. En comparaison de. (En mot composé) En face, à l'encontre. En opposition avec. En échange de, en retour. Au lieu de, à l'égal de. Par correspondance. (En composition) En face, à l'encontre.<br>
'''ἀντίληψις, -ήψεως (nom commun) (f)''' : perception.<br>
'''ἀντιπαράθεσις, -έσεως (nom commun) (f)''' : confrontation.<br>
'''ἀντιπαρατίθημι (verbe)''' : confronter.<br>
'''ἀντίπους, -δός (nom commun) (m)''' : antipode.<br>
'''ἀντίρρησις, -ήσεως (nom commun) (f)''' : objection.<br>
'''ἀντίστοιχος, -ος, -ον (adjectif)''' : correspondant.<br>
'''ἀντιστοίχοτατα, -, - (adverbe)''' : Superlatif de ''ἀντιστοίχως''.<br>
'''ἀντιστοίχοτερον, -, - (adverbe)''' : Comparatif de ''ἀντιστοίχως''.<br>
'''ἀντιστοίχως (adverbe)''' : conformément.<br>
'''ἀντιστοιχῶ (verbe)''' : correspondre.<br>
'''ἀντισυνταγματικός, -ή, -όν (adjectif)''' : inconstitutionnel.<br>
'''ἀντισυνταγματικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀντισυνταγματικός''.<br>
'''ἀντισυνταγματικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀντισυνταγματικός''.<br>
'''ἀντισυνταγματικώτατα, -, - (adverbe)''' : Superlatif de ''ἀντισυνταγματικῶς''.<br>
'''ἀντισυνταγματικώτερον, -, - (adverbe)''' : Comparatif de ''ἀντισυνταγματικῶς''.<br>
'''ἀντισυνταγματικότης, -τος (nom commun) (f)''' : inconstitutionnalité.<br>
'''ἀντισυνταγματικῶς (adverbe)''' : inconstitutionnellement.<br>
'''ἀντίφασις, -άσεως (nom commun) (f)''' : contradiction.<br>
'''ἀντιφάσκω (verbe)''' : contredire.<br>
'''ἀντιφατικός -ή -όν (adjectif)''' : contradictoire.<br>
'''ἀντιφατικῶς (adverbe)''' : contradictoirement.<br>
'''ἀντιφατικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀντισυνταγματικός''.<br>
'''ἀντιφατικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀντισυνταγματικός''.<br>
'''ἀντιφατικώτατα, -, - (adverbe)''' : Superlatif de ''ἀντιφατικῶς''.<br>
'''ἀντιφατικώτερον, -, - (adverbe)''' : Comparatif de ''ἀντιφατικῶς''.<br>
'''ἀντιφατικότης, -τος (nom commun) (f)''' : contradiction.<br>
'''ἀντίχειρ, -ος (nom commun) (m)''' : pouce.<br>
'''ἀντοχή, -ῆς (nom commun) (f)''' : endurance.<br>
'''ἀνυπακοή, -ῆς (nom commun) (f)''' : désobéissance.<br>
'''ἀνυπάκουος, -η, -ον (adjectif)''' : désobéissant.<br>
'''ἀνύψωσις, -ώσεως (nom commun) (f)''' : élévation.<br>
'''ἀνυπακούω (verbe)''' : désobéir.<br>
'''ἀνώδυνος, -ος, -ον (adjectif) ''' : qui n’est pas douloureux. Qui n’est pas causé par la douleur.<br>
'''ἀνώγειον, -ίου (nom commun) (n)''' : grenier.<br>
'''ἀνώτατος, -άτη, -ώτατον (adjectif)''' : suprême.<br>
'''ἀνώτερος, -έρα, -ώτερον (adjectif)''' : supérieur.<br>
'''ἀνωτερότης, -τος (nom commun) (f)''' : supériorité.<br>
'''ἄνω (adverbe)''' : Sur, vers le haut ; au dessus.<br>
'''ἀξιάκουστος, -ος, -ον (adjectif)''' : digne d’être écouté.<br>
'''ἀξίνη, -ης (nom commun) (f)''' : pioche (outil). Hache<br>
'''ἀξιο- (préfixe)''' : qui est digne de.<br>
'''ἀξιοθέατος, -ος, -ον (adjectif)''' : digne d’être contemplé.<br>
'''ἀξιόπιστος, -ος, -ον (adjectif)''' : digne de foi.<br>
'''ἄξιος, -α, -ον (adjectif)''' : de valeur, digne de, méritant.<br>
'''ἀξίωμα, -τος (nom commun) (n)''' : Prix, valeur. Ce dont on a été jugé digne. Considération, estime. Marque de considération, honneur. Haut rang, dignité. Ce que l’on juge convenable, ce qui paraît juste.<br>
'''ἀξιῶ (verbe)''' : mériter (quelque chose).<br>
'''ἄξων, -ονος (nom commun) (m)''' : Axe. Essieu de roue. Axe du ciel, du monde. Axe d’un chemin d’où chemin, route. Crochet du mors d’un cheval. Tablette de bois construite sur un pivot. Arbre ou axe de rotation, pivot, battant balistique.<br>
'''ἀοιδός, -οῦ (nom commun) (m)''' : Chanteur ; chantre.<br>
'''ἀόρατος, -ος, ον (adjectif)''' : invisible.<br>
'''ἄορ, -ος (nom commun) (n)''' : épée pendue à la ceinture.<br>
'''ἀορτήρ, -ῆρος (nom commun) (m)''' : bandoulière.<br>
'''ἀπαγορευτικός, -ή -όν (adjectif)''' : prohibitif.<br>
'''ἀπαγόρευσις, -ύσεως (nom commun) (f)''' : prohibition.<br>
'''ἀπαγορεύω (verbe)''' : prohiber.<br>
'''ἀπαγωγεύς, -έως (nom commun) (m)''' : ravisseur.<br>
'''ἀπαγωγή, -ῆς (nom commun) (f)''' : (Droit) En termes de droit athénien, action d’emmener à un procès un malfaiteur pris en flagrant délit. Action de faire dévier du droit chemin. Paiement (d’une contribution ou d’une amende).<br>
'''ἀπάγω (verbe)''' : conduire ; emmener.<br>
'''ἀπαισιοδοξία, -ας (nom commun) (f)''' : pessimisme.<br>
'''ἀπαίτησις, -ήσεως (nom commun) (f)''' : exigence (ce que l’on exige).<br>
'''ἀπαιτῶ (verbe)''' : exiger.<br>
'''ἀπαλλαγή, -ῆς (nom commun) (f)''' : exonération.<br>
'''ἀπαλλακτικός, -ή -όν (adjectif)''' : exonérant.<br>
'''ἀπαλλάσσω (verbe)''' : exonérer.<br>
'''ἀπαπαῖ (interjection)''' : ouille.<br>
'''ἀπαρνοῦμαι (verbe)''' : .<br>
'''ἀπάτωρ, -ωρ, -ορ (adjectif)''' : sans père.<br>
'''ἄπειμι (verbe)''' : Être absent, s’absenter. Partir, s’en aller.<br>
'''ἀπειρέσιος, -ία, -έσιον (adjectif)''' : Illimité, immense ; innombrable.<br>
'''ἀπειρεσιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀπειρέσιος''.<br>
'''ἀπειρεσιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀπειρέσιος''.<br>
'''ἀπειρεσίως (adverbe)''' : immensément.<br>
'''ἄπειρος, -ος, -ον (adjectif)''' : infini.<br>
'''ἀπειρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἄπειρος''.<br>
'''ἀπειρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἄπειρος''.<br>
'''ἀπείρως (adverbe)''' : infiniment.<br>
'''ἀπελευθέρωσις, -ώσεως (nom commun) (f)''' : libération.<br>
'''ἀπεργάζομαι (verbe)''' : .<br>
'''ἀπέχθεια, -ας (nom commun) (f)''' : répugnance.<br>
'''ἀπεχθής, -ής, -ές (adjectif)''' : répugnant.<br>
'''ἀπήνη, -ης (nom commun) (f)''' : .<br>
'''ἀπίθανος, -ος, -ον (adjectif)''' : improbable ; incroyable.<br>
'''ἄπιον, -ίου (nom commun) (n)''' : poire.<br>
'''ἀπιστία, -ας (nom commun) (f)''' : infidélité ; déloyauté.<br>
'''ἀπιστίη, -ης (nom commun) (f)''' : Forme ionienne de ''ἀπιστία''.<br>
'''ἄπιστος, -ος, -ον (adjectif)''' : infidèle ; déloyal.<br>
'''ἀπλάνεια, -ίας (nom commun) (f)''' : constance.<br>
'''ἄπληστος, -η, -ον (adjectif)''' : .<br>
'''ἁπλότης, -τος (nom commun) (f)''' : simplicité.<br>
'''ἁπλοῦς, -ῆ, -οῦν (adjectif)''' : simple.<br>
'''ἀπό (adverbe ; préposition)''' (Devient ''ἀπ’'' devant un mot commençant par une voyelle à esprit doux, et ''ἀφ’'' devant un mot commençant par une voyelle à esprit rude.) : Au loin, en venant de.<br>
'''ἀπό- (préfixe)''' : séparation, éloignement, changement, achèvement, cessation, retour, privation, négation.<br>
'''ἀπόγειον, -ίου (nom commun) (n)''' : apogée.<br>
'''ἀπόγειος, -ος, -ον (adjectif)''' : Qui part de terre, ou vient de son souffle. Éloigné de la terre.<br>
'''ἀπόγονος, -όνου (nom commun) (m)''' : descendant.<br>
'''ἀπογραφή, -ῆς (nom commun) (f)''' : Registre, liste. Copie.<br>
'''ἀπογράφω (m)''' : copier.<br>
'''ἀπόδειξις, -ίξεως (nom commun) (f)''' : évidence, preuve.<br>
'''ἀποδεικτικός, -ή, -όν (adjectif)''' : évident.<br>
'''ἀποδεικτικῶς (adverbe)''' : évidemment.<br>
'''ἀποδεικτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀποδεικτικός''.<br>
'''ἀποδεικτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀποδεικτικός''.<br>
'''ἀποδεικτικώτατα, -, - (adverbe)''' : Superlatif de ''ἀποδεικτικῶς''.<br>
'''ἀποδεικτικώτερον, -, - (adverbe)''' : Comparatif de ''ἀποδεικτικῶς''.<br>
'''ἀπολογητικός, -ή, -όν (adjectif)''' : défensif.<br>
'''ἀπολογητικῶς (adverbe)''' : défensivement.<br>
'''ἀπολογητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀπολογητικός''.<br>
'''ἀπολογητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀπολογητικός''.<br>
'''ἀπολογητικώτατα, -, - (adverbe)''' : Superlatif de ''ἀπολογητικῶς''.<br>
'''ἀπολογητικώτερον, -, - (adverbe)''' : Comparatif de ''ἀπολογητικῶς''.<br>
'''ἀπολογία, -ας (nom commun) (f)''' : Défense, justification.<br>
'''ἀπόλυτος, -η, -ον (adjectif)''' : absolu.<br>
'''ἀπολύτως (adverbe)''' : absolument.<br>
'''ἀπολύω (verbe)''' : congédier, licencier ; renvoyer.<br>
'''ἀποπομπαῖος, -ία, -ῖον (adjectif)''' : expiatoire.<br>
'''ἀποπέμπω (verbe)''' : emporter le mal.<br>
'''ἀποθνῄσκω (verbe)''' : mourir.<br>
'''ἀποκάλυψις, -ύψεως (nom commun) (f)''' : dévoilement.<br>
'''ἀποκαλύπτω (verbe)''' : dévoiler.<br>
'''ἀποκεφαλισμός, -οῦ (nom commun) (f)''' : décapitation.<br>
'''ἀποκεφαλίζω (verbe)''' : décapiter.<br>
'''ἀποκρουστικός, -ή, -όν (adjectif)''' : . <br>
'''ἀποκρούω (verbe)''' : . <br>
'''ἀπόκρυφος, -ος, -ον (adjectif)''' : . <br>
'''ἀπολαμϐάνω (verbe)''' : jouir. (Avoir l’usage, la possession actuelle de quelque chose.)<br>
'''ἀπόλαυσις, -ύσεως (nom commun) (f)''' : jouissance (Satisfaction voluptueuse ; plaisir né de la relation sexuelle épanouie.)<br>
'''ἀπολαύω (verbe)''' : jouir. (Éprouver un vif plaisir, un orgasme, etc.)<br>
'''ἀπολέγω (verbe)''' : Décliner, refuser.<br>
'''ἀπολογία, -ας (nom commun) (f)''' : Défense, justification.<br>
'''ἀπόλογος, -όγου (nom commun) (m)''' : Défense. Narration, récit détaillé.<br>
'''ἀποπατῶ (verbe)''' : déféquer.<br>
'''ἀποπνίγω (verbe)''' : suffoquer.<br>
'''ἀπόρρηγμα, -ήγματος (nom commun) (n)''' : fragment.<br>
'''ἀπορρίπτω (verbe)''' : .<br>
'''ἀπόσπασμα, -άσματος (nom commun) (n)''' : fragment.<br>
'''ἀποστασία, -ας (nom commun) (f)''' : Révolte, défection ; départ.<br>
'''ἀπόστασις, -άσεως (nom commun) (f)''' : éloignement.<br>
'''ἀφίστημι (verbe)''' : Rejeter, se retirer ; se révolter.<br>
'''ἀποστρέφω (verbe)''' : .<br>
'''ἀποστροφή, -ῆς (nom commun) (f)''' : répulsion.<br>
'''ἀπόστροφος, -ος, -ον (adjectif)''' : répulsif.<br>
'''ἀποτέλεσμα, -έσματος (nom commun) (n)''' : résultat ; effet.<br>
'''ἀποτελῶ (verbe)''' : échouer.<br>
'''ἀποστέρησις, -ήσεως (nom commun) (f)''' : frustration.<br>
'''ἀποστερῶ (verbe)''' : frustrer.<br>
'''ἀποτέφρωσις, -ώσεως (nom commun) (f)''' : incinération.<br>
'''ἀποτεφρῶ (verbe)''' : incinérer.<br>
'''ἀποτυγχάνω (verbe)''' : échouer.<br>
'''ἀποτυχία, -ας (nom commun) (f)''' : échec.<br>
'''ἀπουσία, -ας (nom commun) (f)''' : absence.<br>
'''ἄπους, -ους, -ουν (adjectif)''' : apode.<br>
'''ἀποφαίνω (verbe)''' : décider.<br>
'''ἀπόφασις, -άσεως (nom commun) (f)''' : décision.<br>
'''ἀποφεύγω (verbe)''' : s’échapper.<br>
'''ἀπόφημι (verbe)''' : .<br>
'''ἀποφθέγγομαι (verbe)''' : .<br>
'''ἀπόφθεγμα, -έγματος (nom commun) (n)''' : précepte, sentence.<br>
'''ἀποφθορά, -ᾶς (nom commun) (f)''' : avortement.<br>
'''ἀποφόρητον, -ου (nom commun) (n)''' : étrenne.<br>
'''ἀποφυγή, -ῆς (nom commun) (f)''' : échappée.<br>
'''ἄποψις, -όψεως (nom commun) (f)''' : opinion, point de vue.<br>
'''ἀποψύχω (verbe)''' : s’évanouir.<br>
'''ἁπτικός, -ή, -όν (adjectif)''' : tactile.<br>
'''ἀπύ (adverbe ; préposition)''' : Forme arcado-chypriote et éolienne de ''ἀπό''.<br>
'''ἀπών, -οῦσα, -όν (adjectif)''' : absent.<br>
'''ἅπτω (verbe)''' : toucher.<br>
'''ἅπτω (verbe)''' : ajuster, attacher ; nouer.<br>
'''ἄρα (conjonction)''' : (Devient ''ἄρ’'' devant un mot commençant par une voyelle, et ''ῥά'' après un mot monosyllabique ou un mot finissant par une voyelle.) puis, et, alors. Par suite, ainsi donc, donc. Puisque, à savoir, c’est-à-dire, en effet. Ayant donc, ainsi parlé.<br>
'''ἆρα (particule)''' : (Devient ''ἆρ’'' devant un mot commençant par une voyelle.) est-ce que.<br>
'''ἀρά, -ᾶς (nom commun) (f)''' : Prière. Imprécation.<br>
'''ἄρακος, -άκου (nom commun) (m)''' : pois.<br>
'''ἀράχνη, -ης (nom commun) (f)''' : araignée.<br>
'''ἀρϐύλη, -ης (nom commun) (f)''' : botte (chaussure épaisse au long col).<br>
'''ἄρδις, -ος (nom commun) (f)''' : pointe de flèche.<br>
'''ἀργά (adverbe)''' : tard.<br>
'''ἄργιλλος, -ίλλου (nom commun) (m)''' : argile.<br>
'''ἀργιλλοφόρητος''' : mot fantôme selon Rosane Rocher (1961), comme καλποφόρος, ὀφιοφόρος, ὠποφόρος et σμνρμοφόρος.<br>
'''ἀργιλλώδης (adjectif)''' : argileux.<br>
'''ἀργός, -ή, -όν (adjectif)''' : blanc ; étincelant.<br>
'''ἄργυρος, -ύρου (nom commun) (m)''' : argent. (métal)<br>
'''ἀργῶ (verbe)''' : .<br>
'''ἀρετά, -ᾶς (nom commun) (f)''' : Forme dorienne de ''ἀρετή''.<br>
'''ἀρετή, -ῆς (nom commun) (f)''' : vertu.<br>
'''ἀρή, -ῆς (nom commun) (f)''' : Forme ionienne de ''ἀρά''.<br>
'''ἄρθρον, -ου (nom commun) (n)''' : Jointure, articulation. Article. (outil grammatical)<br>
'''ἀριστερός, -ά, -όν (adjectif)''' : Qui est à gauche.<br>
'''ἀριστόϐουλος, -ος, -ον (adjectif)''' : .<br>
'''ἄριστος, -η, -ον (adjectif)''' : excellent.<br>
'''ἀρκαδικός, -ή, -όν (adjectif)''' : arcadien.<br>
'''ἄρκευθος, -ύθου (nom commun) (f)''' : genévrier.<br>
'''ἄρκτος, -ου (nom commun) (m/f)''' : ours(e).<br>
'''ἄρκυς, -ος (nom commun) (m)''' : filet.<br>
'''ἄρμα, -τος (nom commun) (t)''' : char.<br>
'''ἁρμονία, -ας (nom commun) (f)''' : harmonie.<br>
'''ἁρμονικός, -ή, -όν (adjectif)''' : harmonieux.<br>
'''ἁρμονικῶς (adverbe)''' : harmonieusement.<br>
'''ἁρμονικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἁρμονικός''.<br>
'''ἁρμονικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἁρμονικός''.<br>
'''ἁρμονικώτατα, -, - (adverbe)''' : Superlatif de ''ἁρμονικῶς''.<br>
'''ἁρμονικώτερον, -, - (adverbe)''' : Comparatif de ''ἁρμονικῶς''.<br>
'''ἀρνοῦμαι (verbe)''' : refuser.<br>
'''ἄρουρα, -ας (nom commun) (f)''' : aroure.<br>
'''ἀρουραῖος, -ίου (nom commun) (m)''' : rat.<br>
'''ἁρπακτικός, -οῦ (nom commun) (m)''' : prédateur.<br>
'''ἅρπαξ, -γος (nom commun) (m)''' : rapace, pillard.<br>
'''ἁρπίς, -ῖδος (nom commun) (f)''' : pantoufle.<br>
'''ἀρραϐών, -ῶνος (nom commun) (n)''' : arrhes.<br>
'''ἄρρητος, -ος, -ον (adjectif)''' : Indicible ; ineffable, (Mathématiques) irrationnel.<br>
'''ἄρσην, -ην, -εν (adjectif)''' : Mâle ; dur, fort.<br>
'''ἄρρην, -ενος (nom commun) (m)''' : Forme attique de ''ἄρσην''.<br>
'''ἄρσην, -ενος (nom commun) (m)''' : Homme adulte ; mâle.<br>
'''ἄρσης, -ενος (nom commun) (m)''' : Forme laconienne de ''ἄρσην''.<br>
'''ἄρριχος, -ίχου (nom commun) (m)''' : panier.<br>
'''ἄρρωστος, -ος, -ον (adjectif)''' : .<br>
'''ἀρρωστότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἄρρωστος''.<br>
'''ἀρρωστότερος, -έρα, -ότερον (adjectif)'''' : Comparatif de ''ἄρρωστος''.<br>
'''ἀρρωστότατα, -, - (adverbe)''' : Superlatif de ''ἀρρώστως''.<br>
'''ἀρρωστότερον, -, - (adverbe)''' : Comparatif de ''ἀρρώστως''.<br>
'''ἀρρώστως (adverbe)''' : .<br>
'''ἄρτι (adverbe)''' : justement ; exactement. Maintenant.<br>
'''ἄρτιος, -ία, -ον (adjectif)''' : Parfait, complet ; achevé. (Mathématiques) Pair, en parlant des nombres.<br>
'''ἀρτίως (adverbe)''' : parfaitement ; complètement.<br>
'''ἀρτιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἄρτιος''.<br>
'''ἀρτιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἄρτιος''.<br>
'''ἀρτοποιός, -οῦ (nom commun) (m)''' : boulanger .<br>
'''ἄρτος, -ου (nom commun) (m)''' : pain. (aliment)<br>
'''ἀρτῶ (verbe)''' : Attacher. (Au passif) Dépendre, pendre, être attaché à.<br>
'''ἀρύϐαλλος, ου (nom commun) (m)''' : flacon à huile.<br>
'''ἀρχάγγελος, -έλου (nom commun) (m)''' : messager en chef ; archange.<br>
'''ἀρχαῖος, -ία, -ῖον (adjectif)''' : ancien.<br>
'''ἀρχαιότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἀρχαῖος''.<br>
'''ἀρχαιότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἀρχαῖος''.<br>
'''ἀρχαίως (adverbe)''' : anciennement.<br>
''', -, - (adverbe)''' : Superlatif de ''ἀρχαίως''.<br>
''', -, - (adverbe)''' : Comparatif de ''ἀρχαίως''.<br>
'''ἀρχαϊκός, -ή, -όν (adjectif)''' : vieilli.<br>
'''ἀρχαϊκότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἀρχαϊκός''.<br>
'''ἀρχαϊκότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἀρχαϊκός''.<br>
'''ἀρχαϊκῶς (adverbe)''' : vieillement.<br>
'''ἀρχαϊκώτατα, -, - (adverbe)''' : Superlatif de ''ἀρχαϊκῶς''.<br>
'''ἀρχαϊκώτερον, -, - (adverbe)''' : Comparatif de ''ἀρχαϊκῶς''.<br>
'''ἀρχέτυπον, -ύπου (nom commun) (n)''' : modèle.<br>
'''ἀρχή, -ῆς (nom commun) (f)''' : commencement, commandement.<br>
'''ἀρχηγός, -οῦ (nom commun) (m)''' : chef.<br>
'''-άρχης, -ου (suffixe)''' : qui est chef de.<br>
'''ἀρχι- (préfixe)''' : relatif au commencement, au commandement.<br>
'''ἀρχιδιάκονος, -όνου (nom commun) (m/f)''' : archidiacre.<br>
'''ἀρχιδιήκονος, -όνου (nom commun) (m/f)''' : Forme ionienne de ''ἀρχιδιάκονος''.<br>
'''ἀρχίκλωψ, -πός (nom commun) (m)''' : maître voleur.<br>
'''ἀρχίμιμος, -ίμου (nom commun) (m)''' : archimime.<br>
'''ἀρχιτέκτων, -ονος (nom commun) (m)''' : maître d’œuvre.<br>
'''ἄρχων, -οντος (nom commun) (m)''' : archonte.<br>
'''ἄρωμα, -ώματος (nom commun) (n)''' : parfum.<br>
'''ἄρω (verbe)''' : nouer.<br>
'''ἄσϐολος, -όλου (nom commun) (f)''' : suie.<br>
'''ἀσέϐεια, -ας (nom commun) (f)''' : impiété.<br>
'''ἀσεϐής, -ής, -ές (adjectif)''' : impie.<br>
'''ἀσεϐῶ (verbe)''' : être impie.<br>
'''ἀσέλγεια, -ας (nom commun) (f)''' : lubricité.<br>
'''ἀσελγέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἀσελγής''.<br>
'''ἀσελγέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''ἀσελγής''.<br>
'''ἀσελγής, -ής, -ές (adjectif)''' : lubrique.<br>
'''ἀσελγότατα, -, - (adverbe)''' : Superlatif de ''ἀσελγῶς''.<br>
'''ἀσελγότερον, -, - (adverbe)''' : Comparatif de ''ἀσελγῶς''.<br>
'''ἀσελγῶς (adverbe)''' : lubriquement.<br>
'''ἀσελγῶ (verbe)''' : être lubrique.<br>
'''ἀσθενής, -ής, -ές (adjectif)''' : malade.<br>
'''ἄσθμα, -τος (nom commun) (n)''' : asthme.<br>
'''ἄσις, - (nom commun) (f)''' : Boue, argile.<br>
'''ἀσκάλαϐος, -άϐου (nom commun) (m)''' : gecko.<br>
'''ἄσκαυλος, -ύλου (nom commun) (m)''' : cornemuse.<br>
'''ἀσκέω (verbe)''' : entraîner.<br>
'''ἄσκημα, -ήματος (nom commun) (n)''' : entraînement.<br>
'''ἄσκησις, -ήσεως (nom commun) (f)''' : exercice.<br>
'''ἀσκητεία, -ας (nom commun) (f)''' : ascétie.<br>
'''ἀσκητήριον, -ίου (nom commun) (n)''' : .<br>
'''ἀσκητής, -οῦ (nom commun) (m)''' : ascète.<br>
'''ἀσκητικός, -ή, -όν (adjectif)''' : ascétique.<br>
'''ἀσκός, -οῦ (nom commun) (m)''' : outre.<br>
'''ἀσκῶ (verbe)''' : exercer.<br>
'''ᾆσμα, -τος (nom commun) (n)''' : chant.<br>
'''ἀσπάλαξ, -κος (nom commun) (m)''' : taupe.<br>
'''ἄσπαλος, -άλου (nom commun) (m)''' : squale.<br>
'''ἀσπίς, -δος (nom commun) (f)''' : bouclier.<br>
'''ἀστακός, -οῦ (nom commun) (m)''' : homard.<br>
'''ἀστεῖος, -ία, -ῖον (adjectif)''' : urbain.<br>
'''ἀστήρ, -έρος (nom commun) (m)''' : étoile.<br>
'''ἀστικός, -ή, -όν (adjectif)''' : urbain.<br>
'''ἀστραπή, -ῆς (nom commun) (f)''' : éclair.<br>
'''ἄστρον, -ου (nom commun) (n)''' : Astre, constellation, système d’étoiles.<br>
'''ἀστυνομία, -ας (nom commun) (f)''' : police urbaine.<br>
'''ἀστυνόμος, -ου (nom commun) (m)''' : astynome.<br>
'''ἀστύνομος, -ος, -ον (adjectif)''' : public.<br>
'''ἄστυ, -εως (nom commun) (n)''' : Cité ; ville.<br>
'''ἀστυφύλαξ, -κος (nom commun) (m)''' : policier.<br>
'''ἄσυλον, -ύλου (nom commun) (n)''' : asile.<br>
'''ἄσυλος, -ος, -ον (adjectif)''' : inviolable.<br>
'''ἀσυμμετρία, -ας (nom commun) (f)''' : mauvaise proportion.<br>
'''ἄσυχος, -ος, -ον (adjectif)''' : Forme dorienne de ''ἥσυχος''.<br>
'''ἀσφάλεια, -ίας (nom commun) (f)''' : sécurité.<br>
'''ἀσφαλής, -ής, -ές (adjectif)''' : sûr.<br>
'''ἄσφαλτος, -άλτου (nom commun) (f)''' : asphalte.<br>
'''ἀσφαλῶς (adverbe)''' : sûrement.<br>
'''-άς, -δος (suffixe) (f)''' : Forme des noms d’agent.<br>
'''ἄτα, -ας (nom commun)''' : Forme dorienne de ''ἄτη''.<br>
'''ἄτακτος, -η, -ον (adjectif)''' : vilain.<br>
'''ἀταξία, -ας (nom commun) (f)''' : vilénie.<br>
'''ἀταραξία, -ας (nom commun) (f)''' : Calme ; imperturbabilité.<br>
'''ἅτερος, -έρα, -ερον (adjectif)''' : Forme dorienne de ''ἕτερος''.<br>
'''ἄτερος, -έρα, -ερον (adjectif)''' : Forme éolienne de ''ἕτερος''.<br>
'''ἄτη, -ης (nom commun)''' : Outrage. (Religion) Péché, faute. Ruine.<br>
'''ἀτιμάζω (verbe)''' : déshonorer.<br>
'''ἄτρακτος, -άκτου (nom commun) (n)''' : fuseau.<br>
'''ἀτραπός, -οῦ (nom commun) (f)''' : sentier.<br>
'''ἄττα, -ου (nom commun) (m)''' : Forme homérique de ''πάππας'' et ''τατᾶ''.<br>
'''ἀτύχημα, -ήματος (nom commun) (n)''' : accident.<br>
'''ἀτυχῶ (verbe)''' : avoir un accident.<br>
'''αὐθάδεια, -ας (nom commun) (f)''' : insolence.<br>
'''αὐθάδης, -ής, -ές (adjectif)''' : insolent.<br>
'''αὖθις (adverbe)''' : à nouveau.<br>
'''αὖλαξ, -ὔλακος (nom commun) (m)''' : irrigation.<br>
'''αὐλή, -ῆς (nom commun) (f)''' : cour.<br>
'''αὐλητής, -οῦ (nom commun) (m)''' : joueur d’aulos.<br>
'''αὐλητρίς, -δος (nom commun) (f)''' : joueuse d’aulos.<br>
'''αὐλίζομαι (verbe)''' : passer la nuit.<br>
'''αὖλις, - (nom commun) (f)''' : résidence, camp.<br>
'''αὐλός, -οῦ (nom commun) (m)''' : aulos.<br>
'''αὐλῶ (verbe)''' : jouer de l’aulos.<br>
'''αὐξάνω (verbe)''' : augmenter.<br>
'''αὔξη, -ης (nom commun) (f)''' : croissance.<br>
'''αὔξημα, -ήματος (nom commun) (n)''' : .<br>
'''αὔξησις, -ήσεως (nom commun) (f)''' : Accroissement, hausse.<br>
'''αὔξιμος, -ος, -ον (adjectif)''' : .<br>
'''αὐξητικός, -ή, -όν (adjectif)''' : .<br>
'''αὔξω (verbe)''' : croître.<br>
'''ἀϋπνία, -ας (nom commun) (f)''' : insomnie.<br>
'''αὔρα, -ας (nom commun) (f)''' : aura.<br>
'''αὔρη, -ης (nom commun) (f)''' : Forme ionienne de ''αὔρα''.<br>
'''αὔριον (adverbe)''' : demain, bientôt.<br>
'''αὐστηρός, -ή, -όν (adjectif)''' : sévère.<br>
'''αὐστηρότης, -τος (nom commun) (f)''' : sévérité.<br>
'''αὐστηρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''αὐστηρότατος''.<br>
'''αὐστηρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''αὐστηρότατος''.<br>
'''αὐστηρῶς (adverbe)''' : sévèrement.<br>
'''αὐτανάφλεξις, -έξεως (nom commun) (f)''' : combustion spontanée.<br>
'''αὐτάναξ, -κτος (nom commun) (m)''' : empereur.<br>
'''αὐτάρ (adverbe)''' : mais, cependant.<br>
'''αὐτίκα (adverbe)''' : aussitôt, sur-le-champ.<br>
'''αὐτοκίνητος, -ος, -ον (adjectif)''' : .<br>
'''αὐτοκινῶ (verbe)''' : .<br>
'''αὐτοκράτειρα, -ας (nom commun) (f)''' : impératrice.<br>
'''αὐτοκρατορία, -ας (nom commun) (f)''' : empire.<br>
'''αὐτοκράτωρ, -ορος (nom commun) (m)''' : empereur.<br>
'''αὐτολεξεί (adverbe)''' : mot pour mot.<br>
'''αὐτός -ή, -ό (pronom personnel)''' : il.<br>
'''αὐτοσχεδιάζω (verbe)''' : improviser.<br>
'''αὐτοσχεδιασμός, -οῦ (nom commun) (m)''' : improvisation.<br>
'''αὐτοσχέδιος, -ος, -ον (adjectif)''' : improvisé.<br>
'''αὐτοσχεδίως (verbe)''' : de façon improvisée.<br>
'''αὐτόχθων, -ων, -ον (adjectif)''' : indigène.<br>
'''αὐτοψία, -ας (nom commun) (f)''' : autopsie.<br>
'''αὐχήν, -ένος (nom commun) (m)''' : nuque.<br>
'''αὕω (verbe)''' : soustraire.<br>
'''ἀφαίρεσις, -έσεως (nom commun) (f)''' : soustraction.<br>
'''ἀφαιρῶ (verbe)''' : soustraire.<br>
'''ἀφεδρών, -ῶνος (nom commun) (m)''' : latrine.<br>
'''αφέλεια, -ίας (nom commun) (f)''' : ingénuité, naïveté.<br>
'''ἀφελής, -ής, -ές (adjectif)''' : ingénu, naïf.<br>
'''ἁφή, -ῆς (nom commun) (f)''' : toucher.<br>
'''ἀφήγημα, -ήματος (nom commun) (n)''' : récit.<br>
'''ἀφηγηματικός, -ή -όν (adjectif)''' : narratif.<br>
'''ἀφηγηματικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἀφηγηματικός''.<br>
'''ἀφηγηματικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἀφηγηματικός''.<br>
'''ἀφηγηματικώτατα, -, - (adverbe)''' : Superlatif de ''ἀντιφατικῶς''.<br>
'''ἀφηγηματικώτερον, -, - (adverbe)''' : Comparatif de ''ἀντιφατικῶς''.<br>
'''ἀφήγησις, -ήσεως (nom commun) (f)''' : narration.<br>
'''ἀφηγητής, -οῦ (nom commun) (m)''' : narrateur.<br>
'''ἀφηγοῦμαι (verbe)''' : narrer.<br>
'''ἄφθα, -ης (nom commun) (f)''' : aphte.<br>
'''ἀφίημι (verbe)''' : Envoyer, renvoyer. Laisser aller, lâcher, relâcher. Libérer.<br>
'''ἄφιξις, -ίξεως (nom commun) (f)''' : arrivée.<br>
'''ἀφόδευσις, -ύσεως (nom commun) (f)''' : défécation.<br>
'''ἀφοδεύω (verbe)''' : déféquer.<br>
'''ἀφοπλίζω (verbe)''' : désarmer.<br>
'''ἀφοπλισμός, -οῦ (nom commun) (m)''' : désarmement.<br>
'''ἀφροδισιακός -ή -όν (adjectif)''' : aphrodisiaque.<br>
'''ἀφροδισιασμός, -οῦ (nom commun) (m)''' : aphrodisiasme.<br>
'''ἀφροδισιαστικός, -ή -όν (adjectif)''' : aphrodisiastique.<br>
'''ἀφορισμός, -οῦ (nom commun) (m)''' : abolition, excommunication.<br>
'''ἀφροδίσιος, -α, -ον (adjectif)''' : vénérien.<br>
'''ἀφορίζω (verbe)''' : abolir, excommunier.<br>
'''ἀφοσίωσις, -ώσεως (nom commun) (f)''' : dévotion.<br>
'''ἀφρός, -οῦ (nom commun) (m)''' : écume.<br>
'''ἀφύη, -ῆς (nom commun) (f)''' : anchois.<br>
'''ἀναχαίνω (verbe)''' : retenir son souffle.<br>
'''ἀναχάσκω (verbe)''' : ouvrir la bouche.<br>
'''ἀχαριστῶ (verbe)''' : .<br>
'''ἁχατης, -ου (nom commun) (m)''' : agate.<br>
'''ἀχλυόεις, -εσσα, -εν (adjectif)''' : brumeux.<br>
'''ἀχλυοέντως (adverbe)''' : .<br>
'''ἀχλυοέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἀχλυόεις''.<br>
'''ἀχλυοέστερος, -έρα, -ερον (adjectif)''' : Comparatif de ''ἀχλυόεις''.<br>
'''ἄχλυσις, -εως (nom commun) (f)''' : .<br>
'''ἀχλύς, -ος (nom commun) (f)''' : brume, ténèbres.<br>
'''ἀχλυώδης, -ης, -ες (adjectif)''' : .<br>
'''ἀχλυῶ (verbe)''' : .<br>
'''ἀχράς, -δος (nom commun) (f)''' : poire.<br>
'''ἄχερδος, -έρδου (nom commun) (f)''' : poire.<br>
'''ἀχυρών, -ῶνος (nom commun) (m)''' : grange.<br>
'''ἀψίνθιον, -ίου (nom commun) (n)''' : absinthe.<br>
'''ἄψογος, -ος, -ον (n)''' : impeccable.<br>
'''ἄψ (adverbe)''' : En arrière (sans mouvement), en retour. Encore.<br>
'''ἄωτον, -ου (nom commun) (n)''' : summum.<br>
'''ἄω (verbe)''' : souffler, dormir.<br>
'''Ἀαρών (nom propre) (m)''' : Aaron.<br>
'''Ἀϐαδδών (nom propre) (m)''' : Abaddon.<br>
'''Ἀϐδίας, -ου (nom propre) (m)''' : Abdias.<br>
'''Ἅϐελ (nom propre) (m)''' : Abel.<br>
'''Ἀϐέλλα, -ας (nom propre) (f)''' : Avella.<br>
'''Ἀϐενέζερ (nom propre) (m)''' : Ebenezer.<br>
'''Ἀϐησσυνία, -ας (nom propre) (f)''' : Abyssinie.<br>
'''Ἀϐιά (nom propre) (m)''' : Abija.<br>
'''Ἀϐιάθαρ (nom propre) (m)''' : Abiathar.<br>
'''Ἀϐιγαία, -ας (nom propre) (m)''' : Abigaëlle.<br>
'''Ἀϐραάμ (nom propre) (m)''' : Abraham.<br>
'''Ἀϐράμης, -ου (nom propre) (m)''' : Forme alternative de ''Ἀϐραάμ''.<br>
'''Ἀϐραμίας, -ου (nom propre) (m)''' : Forme alternative de ''Ἀϐραάμ''.<br>
'''Ἄϐραμος, - (nom propre) (m)''' : Forme alternative de ''Ἀϐραάμ''.<br>
'''Ἀγαμέμνων, -ονος (nom propre) (m)''' : Agamemnon.<br>
'''Ἀγαθή, -ῆς (nom propre) (f)''' : Agathe.<br>
'''Ἀγαθοκλῆς, -έους (nom propre) (m)''' : Agathoclès.<br>
'''Ἀγήνωρ, -ορος (nom propre) (m)''' : Agénor.<br>
'''Ἀγησίλαος, -άου (nom propre) (m)''' : Agésilas.<br>
'''Αἰγεύς, -έως (nom propre) (m)''' : Égée.<br>
'''Αἰγιδιός, -οῦ (prénom) (m)''' : Gilles.<br>
'''Αἴγλη, -ης (nom propre) (f)''' : Églé.<br>
'''Αἰγόκερως, -έρωτος (nom commun) (f)''' : Capricorne.<br>
'''Ἄγκυρα, -ύρας (nom propre) (f)''' : Ankara.<br>
'''Ἀγλαΐα, -ας (nom propre) (f)''' : Aglaé.<br>
'''Ἀγλαός, -οῦ (nom propre) (m)''' : Aglaos.<br>
''', - (nom propre) (m)''' : Agrios.<br>
'''Ἁγνοδίκη, -ης (nom propre) (f)''' : Agnodice.<br>
'''Ἁγνωνίδης, -ου (nom propre) (m)''' : .<br>
'''Ἀγρίππας, -α (nom propre) (m)''' : Agrippa.<br>
'''Ἀγριππίνη, -ης (nom propre) (f)''' : Agrippine.<br>
'''Ἀδάμ (nom propre) (m)''' : Adam.<br>
'''ᾍδης, -ου (nom propre) (m)''' : [[wikt:Hadès|Hadès]].<br>
'''Ἀδικία, -ας (nom propre) (f)''' : Adicie.<br>
'''Ἁδριανούπολις, -όλεως (nom propre) (f)''' : Edirne.<br>
'''Ἁδριανός, -οῦ (nom propre) (m)''' : Hadrien.<br>
'''Ἄδωνις, -ώνιδος (nom propre) (m)''' : Adonis.<br>
'''Ἀζαζέλ (nom propre) (m)''' : Azazel.<br>
'''Ἀθανάσιος, -ίου (nom propre) (m)''' : Athanase.<br>
'''Ἀθάνα, -ας (nom propre) (f)''' : Forme dorienne de ''Ἀθηνᾶ''.<br>
'''Ἀθηνᾶ, -ᾶς (nom propre) (f)''' : [[wikt:Athéna|Athéna]].<br>
'''Ἀθῆναι, -ῶν (nom propre) (f)''' : Athènes.<br>
'''Ἀθηναία, -ας (nom propre) (f)''' : Forme de ''Ἀθηνᾶ''.<br>
'''Ἀθηναῖα, -ίας (nom commun) (f)''' : Athénienne.<br>
'''Ἀθηναῖος, -ίου (nom commun) (m)''' : Athénien.<br>
'''Ἀθήναιος, -ίου (nom propre) (m)''' : Athénée.<br>
'''Ἀθήνη, -ης (nom propre) (f)''' : Forme ionienne de ''Ἀθηνᾶ''.<br>
'''Ἀθηνόδωρος, -ώρου (nom propre) (m)''' : Athénodore.<br>
'''Ἀθύρ (nom propre) (m)''' : Athyr.<br>
'''Ἅθωρ (nom propre) (f)''' : Hathor.<br>
'''Ἄθως, -ω (nom propre) (m)''' : Athos (géant) ; mont Athos.<br>
'''Αἰακός, -οῦ (nom propre) (m)''' : Éaque.<br>
'''Αἴας, -ντος (nom propre) (m)''' : Ajax.<br>
'''Αἰαία, -ας (nom propre) (f)''' : Ééa.<br>
'''Ἀΐδας, -ου (nom propre) (m)''' : Forme dorienne de ''ᾍδης''.<br>
'''Ἀΐδης, -ου (nom propre) (m)''' : Forme homérique de ''ᾍδης''.<br>
'''Ἀϊδωνεύς, -έως (nom propre) (m)''' : Forme de ''ᾍδης''.<br>
'''Αἰγεύς, -έως (nom propre) (m)''' : Égée.<br>
'''Αἰγιδιός, -οῦ (prénom) (m)''' : Gilles.<br>
'''Αἴγισθος, -ίσθου (nom propre) (m)''' : Égisthe.<br>
'''Αἰγύπτια, -ίας (nom commun) (m)''' : Égyptienne.<br>
'''Αἰγυπτιακή, -ῆς (nom propre) (f)''' : égyptien (langue).<br>
'''Αἰγύπτιος, -ίου (nom commun) (m)''' : Égyptien.<br>
'''Αἴγυπτος, -ύπτου (nom propre) (m)''' : Égypte.<br>
'''Αἰήτης, -ου (nom propre) (m)''' : Éétès.<br>
'''Αἰκατερίνη, -ης (prénom) (f)''' : Catherine.<br>
'''Αἰθαλία, -ας (nom propre) (f)''' : Italie.<br>
'''Αἰθήρ, -έρος (nom propre) (m)''' : [[wikt:Éther|Éther]].<br>
'''Αἰθιοπία, -ας (nom propre) (f)''' : Éthiopie.<br>
'''Αἰθιοπίς, -δος (nom commun) (f)''' : Éthiopienne.<br>
'''Αἰθίοψ, -πος (nom commun) (m)''' : Éthiopien.<br>
'''Αἰμιλία, -ας (nom propre) (f)''' : Émilie.<br>
'''Αἰμίλιος, -ίου (nom propre) (m)''' : Émile.<br>
'''Αἰνέας, -ου (nom propre) (m)''' : Forme poétique de ''Αἰνείας''.<br>
'''Αἰνείας, -ου (nom propre) (m)''' : Énée.<br>
'''Αἰνειάς, -δος (nom propre) (f)''' : Énéide.<br>
'''Αἰολίς, -δος (nom propre) (f)''' : Éolide.<br>
'''Αἰολεύς, -έως (nom commun) (m)''' : Éolien.<br>
'''Αἴολος, -όλου (nom propre) (m)''' : [[wikt:Éole|Éole]].<br>
'''Αἴολος, -όλου (nom propre) (m)''' : Éole (fils d'Hellen).<br>
'''Αἶσα, -ἴσης (nom propre) (f)''' : [[wikt:Ésa|Ésa]].<br>
'''Αἴσακος, -άκου (nom propre) (m)''' : Ésaque,
'''Αἰσχύλος, -ου (nom propre) (m)''' : Eschyle.<br>
'''Αἴσωπος, -ώπου (nom propre) (m)''' : Ésope.<br>
'''Αἴτνη, -ης (nom propre) (f)''' : Etna.<br>
'''Αἰών, -ῶνου (nom propre) (m)''' : Éon.<br>
'''Ἀκαδημία, -ας (nom propre) (f)''' : jardin d’Académos, près d’Athènes, où Platon enseignait.<br>
'''Ἀκάδημος, -ήμου (nom propre) (m)''' : Académos.<br>
'''Ἄκις, -εως (nom propre) (m)''' : Acis.<br>
'''Ἀκταίων, -ος (nom propre) (m)''' : Actéon.<br>
'''Ἀκτέων, -ος (nom propre) (m)''' : Forme poétique d’''Ἀκταίων''.<br>
'''Ἄκτωρ, -ορος (nom propre) (m)''' : Actor (frère cadet d’Augias).<br>
'''Ἀκκώ, -οῦς (nom propre) (f)''' : Aléria.<br>
'''Ἀλαλίη, -ης (nom propre) (f)''' : Aléria.<br>
'''Ἀλϐίων, -ος (nom propre) (m)''' : Albion.<br>
'''Ἀλεξάνδρεια, -ίας (nom propre) (f)''' : Alexandrie.<br>
'''Ἀλεξανδρέττα, -ας (nom commun) (f)''' : Alexandrine.<br>
'''Ἀλεξανδρεύς, -έως (nom commun) (m)''' : Alexandrin.<br>
'''Ἀλέξανδρος, -άνδρου (nom propre) (m)''' : Alexandre.<br>
'''Ἀλέξανδρος ὁ Μέγας (nom propre) (m)''' : Alexandre le Grand.<br>
'''Ἄλεξις, - (nom propre) (m)''' : Alexis.<br>
'''Ἀληκτώ, -οῦς (nom propre) (f)''' : Alecto (une des Érynies).<br>
'''Ἀλήτης, -ου (nom propre) (m)''' : Alétès.<br>
'''Ἀλθαία, -ας (nom propre) (f)''' : Althée.<br>
'''Ἀλιλάτ (nom propre) (f)''' : Al-Lat.<br>
'''Ἀλκάθοος, -ου (prénom) (m)''' : Alcathoos.<br>
'''Ἀλκείδης, ου (nom propre) (m)''' : Alcide (Premier nom d’Héraclès.).<br>
'''Ἄλκης, -ους (nom propre) (m)''' : Alceste.<br>
'''Ἄλκηστις, -ήστιδος (nom propre) (f)''' : Alceste.<br>
'''Ἀλκιϐιάδης, ου (nom propre) (m)''' : Alcibiade.<br>
'''Ἀλκίνοος, -ου (nom propre) (m)''' : Alcinoos.<br>
'''Ἀλκμαίων, -ος (nom propre) (m)''' : Alcméon.<br>
'''Ἀλκμήνη, -ης (nom propre) (f)''' : Alcmène.<br>
'''Ἀλφιτώ, -οῦς (nom propre) (f)''' : Alphito.<br>
'''Ἀλώπηξ, -εκος (nom propre) (f)''' : Vulpecula.<br>
'''Ἀμϐρόσιος, -ίου (nom propre) (m)''': Ambroise.<br>
'''Ἀμαζών, -όνος (nom propre) (f)''' : Amazone.<br>
'''Ἀμένωφις, - (nom propre) (m)''': Aménophis.<br>
'''Ἄμηστρις, -δος (nom propre) (f)''' : Amestris.<br>
'''Ἀμαύνι (nom propre) (f)''' : Amemet.<br>
'''Ἀμπρακία, -ας (nom propre) (f)''' : Ambracie.<br>
'''Ἀμπρακιώτης, -ου (nom commun) (m)''' : Ambracien.<br>
'''Ἀμπρακιῶτις, -ώτιδος (nom commun) (f)''' : Ambracienne.<br>
'''Ἀμύντας, -ου (nom propre) (m)''' : Amyntas.<br>
'''Ἀμφιμέδων, -οντος (nom propre) (m)''' : Amphimédon.<br>
'''Ἀμφιστρεύς, -έως (nom propre) (m)''' : Amphitrée.<br>
'''Ἀμφιτρίτη, -ης (nom propre) (f)''' : Amphitrite.<br>
'''Ἀμφιτροπή, -ῆς (nom propre) (f)''' : Amphitropée.<br>
'''Ἀμφιτρύων, -ωνος (nom propre) (m)''' : Amphitryon.<br>
'''Ἀναξίϐια, -ας (nom propre) (f)''' : Anaxibie.<br>
'''Ἀναξίϐιος, -ίου (nom propre) (m)''' : Anaxibios.<br>
'''Ἀννίϐας, -ου (nom propre) (m)''' : Hannibal.<br>
'''Ἀνδρέας, -ου (prénom) (m)''' : André.<br>
'''Ἄννα, -ας (prénom) (f)''' : Anne.<br>
'''Ἀνάγκη, -ης (nom propre) (f)''' : Ananké.<br>
'''Ἀναῗτις, -ΐτεως (nom propre) (f)''' : Anaïs.<br>
'''Ἀναξαγόρας, -ου (nom propre) (m)''' : Anaxagore.<br>
'''Ἀναξίμανδρος, -ου (nom propre) (m)''' : Anaximandre.<br>
'''Ἀναξιμήνης, -ου (nom propre) (m)''' : Anaximène.<br>
'''Ἀναστασία, -ας (nom propre) (f)''' : Anastasie.<br>
'''Ἀναστάσιος, -ίου (nom propre) (m)''' : Anastase.<br>
'''Ἀνατόλιος, -ίου (nom propre) (m)''' : Anatole.<br>
'''Ἄνουϐις, -ύϐιδος (nom propre) (m)''' : [[wikt:Anubis|Anubis]].<br>
'''Ἄνουκις, -δος (nom propre) (f)''' : Anoukis.<br>
'''Ἀντέρως, -τος (nom propre) (m)''' : [[wikt:Antéros|Antéros]].<br>
'''Ἀντιγόνη, -ης (nom propre) (f)''' : Antigone.<br>
'''Ἀντίκλεια, -ας (nom propre) (f)''' : Anticlée (mère d'Ulysse).<br>
'''Ἀντικύθηρα, -ήρων (nom propre) (n)''' : Anticythère.<br>
'''Ἀντίνοoς, -όου (nom propre) (m)''' : Antinoüs.<br>
'''Ἀντίπολις, -όλεως (nom propre) (f)''' : Antipolis.<br>
'''Ἀντισθένης, -ους (nom propre) (m)''' : Antisthène. (Philosophe né vers 444 av. J.-C. et décédé vers 365 av. J.-C.)<br>
'''Ἀντωνία, -ας (nom propre) (f)''' : Antonia.<br>
'''Ἀντώνιος, -ίου (nom propre) (m)''' : Antoine.<br>
'''Ἄντων, -ου (nom propre) (m)''' : Anton.<br>
'''Ἀξιός, -οῦ (nom propre) (m)''' : Axius.<br>
'''Ἀπάτη, -ης (nom propre) (f)''' : Apaté.<br>
'''Ἀπιδανός, -οῦ (nom propre) (m)''' : Apidanus.<br>
'''Ἆπις, Ἄπιδος (nom propre)''' : Apis.<br>
'''Ἀπολλόδωρος, -ώρου (nom propre) (m)''' : Apollodore.<br>
'''Ἀπόλλων, -ωνος (nom propre) (m)''' : [[wikt:Apollon|Apollon]].<br>
'''Ἀρά, -ᾶς (nom propre) (f)''' : [[wikt:Ara|Ara]].<br>
'''Ἀραϐία, -ας (nom propre) (f)''' : Arabie.<br>
'''Ἀράχνη, -ης (nom propre) (f)''' : Arachné.<br>
'''Ἄραψ, -ϐος (nom commun) (m/f)''' : Arabe.<br>
'''Ἀραράτ (nom propre) (m)''' : Ararat.<br>
'''Ἀργαῖος, -ίου (nom propre) (m)''' : Argaïos.<br>
'''Ἀργοναῦτης, -ύτου (nom commun) (m)''' : Argonaute.<br>
'''Ἀργῷος, -ῴος, -ῷον (adjectif)''' : .<br>
'''Ἀργώ, -οῦς (nom propre) (f)''' : Argo.<br>
'''Ἀρετή, -ῆς (nom propre) (f)''' : Arété (déesse).<br>
'''Ἄρευς, -εως (nom propre) (m)''' : Forme éolienne de ''Ἄρης''.<br>
'''Ἄρης, -εως (nom propre) (m)''' : [[wikt:Arès|Arès]].<br>
'''Ἀρήτη, -ης (nom propre) (f)''' : Arété (philosophe grecque de l'école des Cyrénaiques).<br>
'''Ἀρίσταρχος, -άρχου (nom commun) (m)''' : Aristarque.<br>
'''Ἀριστόϐουλος, -ύλου (nom propre) (m)''' : Aristobule.<br>
'''Ἀριστογείτων, -ονος (nom propre) (m)''' : Aristogiton.<br>
'''Ἀριστοτέλης, -ους (nom propre) (m)''' : Aristote.<br>
'''Ἀριστώνυμος, -ύμου (nom propre) (m)''' : Aristonyme.<br>
'''Ἀρίων, -ονος (nom propre) (m)''' : Arion.<br>
'''Ἀρκαδία, -ας (nom propre) (f)''' : Arcadie.<br>
'''Ἀρκάς, -δος (nom commun) (m/f)''' : Arcadien ; Arcadienne.<br>
'''Ἀρκτοῦρος, -ύρου (nom propre) (m)''' : Arcturus.<br>
'''Ἁρμαγεδών (nom propre) (m)''' : Armageddon.<br>
'''Ἁρμόδιος, -ίου (nom propre) (m)''' : Harmodios.<br>
'''Ἀρσένιος, -ίου (nom propre) (m)''' : Arsène.<br>
'''Ἀρσινόη, -ης (nom propre) (f)''' : Arsinoé.<br>
'''Ἀρταξέρξης, -ου (nom propre) (m)''' : Artaxerxès.<br>
'''Ἄρτεμις, -έμιδος (nom propre) (f)''' : [[wikt:Artémis|Artémis]].<br>
'''Ἀρχίας, -ου (nom propre) (m)''' : Archias.<br>
'''Ἀρχιμήδης, -ους (nom propre) (m)''' : Archimède.<br>
'''Ἀσάνα, -ας (nom propre) (f)''' : Forme dorienne de ''Ἀθηνᾶ''.<br>
'''Ἀσδρούϐας, -ου (nom propre) (m)''' : Hasdrubal (véritable nom de Clitomaque de Carthage).<br>
'''Ἀσήρ (nom propre) (m)''' : Aser.<br>
'''Ἀσκαλωνίτης, -ου (nom commun) (m)''' : Ascalonite.<br>
'''Ἀσκαλωνῖτις, -ίτιδος (nom commun) (f)''' : Ascalonite.<br>
'''Ἀσκάλων, -ος (nom propre) (m)''' : Ascalon.<br>
'''Ἀσκληπιός, -οῦ (nom propre) (m)''' : [[wikt:Asclépios|Asclépios]].<br>
'''Ἀσμοδαῖος, -ίου (nom propre) (m)''' : Asmodée.<br>
'''Ἀσεννέθ (nom propre) (m)''' : Asnath.<br>
'''Ἀσπαθίνης, -ου (nom propre) (m)''' : Aspathinès.<br>
'''Ἀσουήρος, -ου (nom propre) (m)''' : Assuérus.<br>
'''Ἀσσυρία, -ας (nom propre) (f)''' : Assyrie.<br>
'''Ἀσσύριος, -ίου (nom commun) (m)''' : Assyrien.<br>
'''Ἀστάρτη, -ης (nom propre) (f)''' : Astarté.<br>
'''Ἀστυάναξ, -κτος (nom propre) (m)''' : Astyanax.<br>
'''Ἀστυάνασσα, -ας (nom propre) (f)''' : Astyanassa.<br>
'''Ἄστυ, -εως (nom propre) (n)''' : Athènes.<br>
'''Ἄτη, -ης (nom propre) (f)''' : [[wikt:Até|Até]]. (Déesse de l’égarement.)<br>
'''Ἀττική, -ῆς (nom propre) (f)''' : Attique.<br>
'''Ἀτλαντίς, -δος (nom propre) (f)''' : (Toponymie) océan Atlantique. (Mythologie) Atlantide.<br>
'''Ἄτλας, -αντος (nom propre) (m)''' : Atlas.<br>
'''Ἄτροπος, -όπου (nom propre) (f)''' : Atropos (troisième Moire).<br>
'''Αὐγείας, -ου (nom propre) (m)''' : Augias.<br>
'''Αὐλίς, -δος (nom propre) (f)''' : Aulis.<br>
'''Αὐνάν (nom propre) (m)''' : Onan.<br>
'''Αὐξώ, -οῦς (nom propre) (f)''' : Auxo.<br>
'''Αὐσονία, -ας (nom propre) (f)''' : Italie.<br>
'''Αὐσονίη, -ης (nom propre) (f)''' : Forme alternative de ''Αὐσονία''.<br>
'''Αὔσων, -ονος (nom propre) (m)''' : Auson (fils d’Ulysse.)<br>
'''Αὐτόλυκος, -ύκου (nom propre) (m)''' : Autolycos (Aïeul maternel d’Ulysse.)<br>
'''Αὐτομέδων, -οντος (nom propre) (m)''' : Automédon (Conducteur du char d’Achille.)<br>
'''Αὔως, -ω (nom propre) (f)''' : Forme alternative de ''Ἕως''.<br>
'''Ἀφρική, -ῆς (nom propre) (f)''' : Afrique.<br>
'''Ἀφροδίτη, -ης (nom propre) (f)''' : Aphrodite.<br>
'''Ἀφρόδιτος, -ίτου (nom propre) (m)''' : Aphroditos.<br>
'''Ἀφρώ, -οῦς (nom propre) (f)''' : Aphrô.<br>
'''Ἀχαΐα, -ας (nom propre) (f)''' : Achaïe.<br>
'''Ἀχαιμένης, -ους (nom propre) (m)''' : Achéménès.<br>
'''Ἀχαιμενίδης, -ου (nom propre) (m)''' : Achéménide.<br>
'''Ἀχαιός, -οῦ (nom propre) (m)''' : Achaïos (fils de Xouthos).<br>
'''Ἀχατης, -ου (nom propre) (m)''' : Achatès (fleuve de Sicile).<br>
'''Ἀχελῷος, -ῴου (nom propre) (m)''' : Achéloos.<br>
'''Ἀχέρων, -οντος (nom propre) (m)''' : Achéron.<br>
'''Ἀχιλλεύς, -έως (nom propre) (m)''' : Achille.<br>
'''Ἀωσφόρος, -ου (nom propre) (m)''' : Forme dorienne de ''Ἑωσφόρος''.<br>
==Β==
'''βαθμίς, -δος (nom commun) (f)''' : échelon.<br>
'''βάθρον, -ου (nom commun) (n)''' : podium.<br>
'''βαθέως (adverbe)''' : profondément.<br>
'''βάθος, -ους (nom commun) (n)''' : Profondeur, hauteur.<br>
'''βαθύς, -εῖα, -ύ (adjectif)''' : profond.<br>
'''βαθύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''βαθύς''.<br>
'''βαθύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''βαθύς''.<br>
'''βαΐον, -ου (nom commun) (n)''' : rameau.<br>
'''βακτηρία, -ας (nom commun) (f)''' : Bâton de marche ; bâton employé comme insigne de juge.<br>
'''βαλανεῖον, -ίου (nom commun) (n)''' : bain (lieu public).<br>
'''βαλανεύς, -έως (nom commun) (n)''' : plongeur.<br>
'''βάλανος, -άνου (nom commun) (f)''' : Gland de chêne. (Botanique) Datte. Fermoir d’un collier. Pêne d’un verrou. Moule de mer (poisson). Noix, châtaigne. Suppositoire.<br>
'''βαλάντιον, -ίου (nom commun) (n)''' : bourse (sac d'argent).<br>
'''βαλλίζω (verbe)''' : danser.<br>
'''βάμϐαξ, -κος (nom commun) (m)''' : coton.<br>
'''βαμϐακερός, -ή, -όν (adjectif)''' : .<br>
'''βαμϐακηρός, -ά, -όν (adjectif)''' : .<br>
'''βαμϐάκινος, -ίνου (nom commun) (n)''' : .<br>
'''βαμϐάκιον, -ίου (nom commun) (n)''' : .<br>
'''βαμϐακοειδής, -ής, -ές (adjectif)''' : .<br>
'''βανά, -ᾶς (nom commun) (f)''' : Forme béotienne de ''γυνή''.<br>
'''βάπτω (verbe)''' : plonger, teindre ; baptiser.<br>
'''βάρϐαρα, -ας (nom commun) (f)''' : barbare.<br>
'''βάρϐαρικός, -ή, -όν (adjectif)''' : barbare.<br>
'''βάρϐαρικῶς (adverbe)''' : barbarement.<br>
'''βάρϐαρικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''βάρϐαρικός''.<br>
'''βάρϐαρικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''βάρϐαρικός''.<br>
'''βάρϐαρικώτατα, -, - (adverbe)''' : Superlatif de ''βάρϐαρικῶς''.<br>
'''βάρϐαρικώτερον, -, - (adverbe)''' : Comparatif de ''βάρϐαρικῶς''.<br>
'''βάρϐαρος, -άρου (nom commun) (m)''' : barbare.<br>
'''βαρέως (adverbe)''' : lourdement.<br>
'''βᾶρις, -άριδος (nom commun) (f)''' : barque.<br>
'''βᾶρκις, -άρκιδος (nom commun) (f)''' : barque.<br>
'''βαρύς, -εῖα, -ύ (adjectif)''' : lourd.<br>
'''βαρύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''βαρύς''.<br>
'''βαρύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''βαρύς''.<br>
'''βασιλεία, -ας (nom commun) (f)''' : pouvoir royal ; royauté. Royaume.<br>
'''βασίλεια, -ίας (nom commun) (f)''' : princesse. (héritière du souverain régnant)<br>
'''βασιλείδης, -ου (nom commun) (m)''' : prince. (héritier du souverain régnant)<br>
'''βασίλειος, -ία, -ιον (adjectif)''' : royal.<br>
'''βασιλεύς, -έως (nom commun) (m)''' : roi.<br>
'''βασιλήϊος, -, - (adjectif)''' : Forme ionienne de ''βασίλειος''.<br>
'''βασιλῇος, -, - (adjectif)''' : Forme éolienne de ''βασίλειος''.<br>
'''βασιλικός, -ή, -όν (adjectif)''' : royal.<br>
'''βασίλισσα, -ας (nom commun) (f)''' : reine.<br>
'''βάσις, -εως (nom commun) (f)''' : Action de marcher, marche. Organe pour la marche. Ce sur quoi l’on marche ou l’on se tient.<br>
'''βάσκανος, -ος, -ον (adjectif)''' : Maléfique ; médisant.<br>
'''βασκάνως (adverbe)''' : maléfiquement.<br>
'''βασκανώτατος, -, - (adverbe)''' : Superlatif de ''βάσκανος''.<br>
'''βασκανώτερος, -, - (adverbe)''' : Comparatif de ''βάσκανος''.<br>
'''βασσάρα, -ας (nom commun) (f)''' : renard.<br>
'''βαστάζω (verbe)''' : lever, soulever. Relever. Tenir dans ses bras ou ses mains.<br>
'''βάτος, -ου (nom commun) (f)''' : Ronce, buisson. Épine.<br>
'''βατός, -ή, -όν (adjectif)''' : accessible.<br>
'''βάτραχος, -άχου (nom commun) (m/f)''' : Crapaud ; grenouille.<br>
'''βατῶ (verbe)''' : Forme phocienne de ''πατῶ''.<br>
'''βαυϐάω (verbe)''' : s’endormir.<br>
'''βαυϐών, -ῶνος (nom commun) (m)''' : godemichet.<br>
'''βαυϐώ, -οῦς (nom commun) (f)''' : nourrice.<br>
'''βαυκάλημα, -ήματος (nom commun) (n)''' : berceuse.<br>
'''βαυκίζομαι (verbe)''' : être prude.<br>
'''βαυκός, -ή, -όν (adjectif)''' : prude.<br>
'''βαυκότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''βαυκός''.<br>
'''βαυκότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''βαυκός''.<br>
'''βαυκῶς (adverbe)''' : prudemment.<br>
'''βαυκώτατα, -, - (adverbe)''' : Superlatif de ''βαυκῶς''.<br>
'''βαυκώτερον, -, - (adverbe)''' : Comparatif de ''βαυκῶς''.<br>
'''βαύ (onomatopée)''' : ouaf.<br>
'''βαφεύς, -έως (nom commun) (m)''' : peintre.<br>
'''βδαλεύς, -έως (nom commun) (m)''' : suceur.<br>
'''βδάλσις, - (nom commun) (f)''' : succion.<br>
'''βδάλλω (verbe)''' : Sucer, téter. Traire le lait.<br>
'''βδέλλα, -ης (nom commun) (f)''' : sangsue.<br>
'''βδέω (verbe)''' : péter (flatuler)<br>
'''βεϐαιῶ (verbe)''' : garantir (se rendre garant de la valeur, de la qualité d’une chose).<br>
'''βέϐηλος, -ος, -ον (adjectif)''' : Franchissable, profane. Impur, interdit.<br>
'''βεϐηλότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''βέϐηλος''.<br>
'''βεϐηλότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''βέϐηλος''.<br>
'''βέϐηλως (adverbe)''' : profanement.<br>
'''βεϐηλώτατα, -, - (adverbe)''' : Superlatif de ''βεϐρῶς''.<br>
'''βεϐηλώτερον, -, - (adverbe)''' : Comparatif de ''βεϐρῶς''.<br>
'''βεϐρός, -ά, -όν (adjectif)''' : stupide.<br>
'''βεϐρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''βεϐρός''.<br>
'''βεϐρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''βεϐρός''.<br>
'''βεϐρῶς (adverbe)''' : stupidement.<br>
'''βεϐρώτατα, -, - (adverbe)''' : Superlatif de ''βεϐρῶς''.<br>
'''βεϐρώτερον, -, - (adverbe)''' : Comparatif de ''βεϐρῶς''.<br>
'''βελόνη, -ης (nom commun) (f)''' : aiguille.<br>
'''βέλος, -ους (nom commun) (n)''' : flèche.<br>
'''βέλτιστος, -ίστη, -έλτιστον (adjectif)''' : Superlatif de ''ἀγαθός''.<br>
'''βελτίων, -ων, -έλτιον (adjectif)''' : Comparatif de ''ἀγαθός''.<br>
'''βένθος, -ους (nom commun) (n)''' : benthos.<br>
'''βερϐέριον, -ίου (nom commun) (n)''' : .<br>
'''βεῦδος, -ύδους (nom commun) (n)''' : .<br>
'''βέφυρα, -ύρας (nom commun) (f)''' : Forme béotienne de ''γέφυρα ''.<br>
'''βῆ βῆ (onomatopée)''' : bêlement.<br>
'''βῆμα, -ήματος (nom commun) (n)''' : .<br>
'''βήξ, -χός (nom commun) (m)''' : toux.<br>
'''βήσσω (verbe)''' : tousser.<br>
'''βῆτα (nom commun) (n)''' : bêta.<br>
'''βήττω (verbe)''' : Forme attique ''βήσσω''.<br>
'''βία, -ας (nom commun) (f)''' : Force, violence.<br>
'''βιάζω (verbe)''' : Contraindre, forcer. (Au passif) Être contraint, forcé soumis.<br>
'''βιϐάζω (verbe)''' : Faire aller ; exalter.<br>
'''βιϐλαρίδιον, -ίου (nom commun) (n)''' : livret.<br>
'''βιϐλιοθήκη, -ης (nom commun) (f)''' : bibliothèque.<br>
'''βιϐλίον, -ου (nom commun) (n)''' : livre.<br>
'''βιϐλιοπωλεῖον, -ίου (nom commun) (n)''' : librairie.<br>
'''βιϐλιοπώλης, -ου (nom commun) (m)''' : libraire.<br>
'''βίϐλος, -ου (nom commun) (f)''' : Papier fait à partir de l'écorce de papyrus.<br>
'''βιϐρώσκω (verbe)''' : Dévorer, manger avec avidité. (Figuré) Dévorer ou engloutir (une fortune).<br>
'''βίος, -ου (nom commun) (m)''' : vie.<br>
'''βίωσις, -ώσεως (nom commun) (f)''' : manière de vie.<br>
'''βινῶ (verbe)''' : foutre, baiser (avoir un rapport sexuel).<br>
'''βιῶ (verbe)''' : être, vivre.<br>
'''βλαδαρός, -ά, -όν (adjectif)''' : flasque.<br>
'''βλαδύς, -εῖα, -ύ (adjectif)''' : faible.<br>
'''βλαδυτής, -ῆτος (nom commun) (f)''' : faiblesse.<br>
'''βλακότατα, -, - (adverbe)''' : Superlatif de ''βλακωδῶς''.<br>
'''βλακότερον, -, - (adverbe)''' : Comparatif de ''βλακωδῶς''.<br>
'''βλακωδῶς (adverbe)''' : stupidement.<br>
'''βλακώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''βλάξ''.<br>
'''βλακώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''βλάξ''.<br>
'''βλαπτικός, -ή, -όν (adjectif)''' : .<br>
'''βλαπτικῶς (adverbe)''' : -ment.<br>
'''βλαπτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''βλαπτικός''.<br>
'''βλαπτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''βλαπτικός''.<br>
'''βλαπτικώτατα, -, - (adverbe)''' : Superlatif de ''βλαπτικῶς''.<br>
'''βλαπτικώτερον, -, - (adverbe)''' : Comparatif de ''βλαπτικῶς''.<br>
'''βλάπτω (verbe)''' : Léser, endommager. (Au passif) Éprouver un accident. Gêner, embarrasser. (En parlant de l’esprit) Troubler sa raison. (Postérieur) Faire du tort, nuire.<br>
'''βλάξ, -κός (nom commun) (m/f)''' : stupide.<br>
'''βλαισός, -ή, -όν (adjectif)''' : balbutiant.<br>
'''βλέμμα, -τος (nom commun) (n)''' : Regard. (Au pluriel) Yeux.<br>
'''βλεννογόνος, -ου (nom commun) (m)''' : muqueuse.<br>
'''βλεννός, -οῦ (nom commun) (m)''' : mucus.<br>
'''βλέπω (verbe)''' : voir.<br>
'''βλεφαρίς, -δος (nom commun) (f)''' : cil.<br>
'''βλεφαρῖτις, -ίτης (nom commun) (f)''' : blépharite.<br>
'''βλέφαρον, -άρου (nom commun) (n)''' : paupière.<br>
'''βλῆμα, -ήματος (nom commun) (n)''' : projectile.<br>
'''βοή, -ῆς (nom commun) (f)''' : clameur.<br>
'''βοήθεια, -ας (nom commun) (f)''' : aide.<br>
'''βοηθός, -ός, -όν (adjectif)''' : aidant.<br>
'''βοηθῶ (verbe)''' : aider.<br>
'''βοιώτιος, -, - (adjectif)''' : béotien.<br>
'''βόλλα, -ας (nom commun) (f)''' : Forme éolienne de ''βουλή''.<br>
'''βορά, -ᾶς (nom commun) (f)''' : proie.<br>
'''βόρειος, -ία, -ιον (adjectif)''' : septentrional.<br>
'''βορειώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''βόρειος''.<br>
'''βορειώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''βόρειος''.<br>
'''βορειώτατα, -, - (adverbe)''' : Superlatif de ''βόρειως''.<br>
'''βορειώτερον, -, - (adverbe)''' : Comparatif de ''βόρειως''.<br>
'''βόρειως (adverbe)''' : septentrionalement.<br>
'''βόμϐος, -ου (nom commun) (m)''' : bourdonnement.<br>
'''βομϐυλιός, -οῦ (nom commun) (m)''' : .<br>
'''βόμϐυξ, -κος (nom commun) (m)''' : ver à soie ; sorte de flûte.<br>
'''βόρειος, -α, -ον (adjectif)''' : septentrional.<br>
'''βορρᾶς, -ᾶ (nom commun) (m)''' : nord.<br>
'''βόσκω (verbe)''' : Nourrir. Paître, se nourrir (en parlant des animaux)<br>
'''βόστρυχος, -ύχου (nom commun) (m)''' : boucle de cheveux.<br>
'''βοτάνη, -ης (nom commun) (f)''' : herbe.<br>
'''βοτανηφάγος, -ος, -ον (adjectif)''' : herbivore.<br>
'''βοτανηφόρος, -ος, -ον (adjectif)''' : enherbé.<br>
'''βοτανίδιον, -ίου (nom commun) (n)''' .<br>
'''βοτανίζω (verbe)''' : désherber.<br>
'''βοτάνιον, -ίου (nom commun) (n)''' : .<br>
'''βοτανικός, -ή, -όν (adjectif)''' : botanique.<br>
'''βοτανισμός, -οῦ (nom commun) (m)''' : botanisme.<br>
'''βοτανολογία ''' : botanologie.<br>
'''βοτανολόγος ''' : herboriste.<br>
'''βοτανολογῶ (verbe)''' : herboriser.<br>
'''βοτανώδης, -ης, -ης (adjectif)''' : botanique.<br>
'''βοτήρ, -ῆρος (nom commun) (m)''' : berger ; pasteur.<br>
'''βοτόν, -οῦ (nom commun) (n)''' : bétail.<br>
'''βουϐών, -ῶνος (nom commun) (m)''' : aine.<br>
'''βουίζω (verbe)''' : bourdonner.<br>
'''βουκολικός, -ή, -όν (adjectif)''' : pastoral.<br>
'''βουκολικῶς (adverbe)''' : pastoralement.<br>
'''βουκολικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''βουκολικικός''.<br>
'''βουκολικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''βουκολικικός''.<br>
'''βουκολικώτατα, -, - (adverbe)''' : Superlatif de ''βουκολικικῶς''.<br>
'''βουκολικώτερον, -, - (adverbe)''' : Comparatif de ''βουκολικικῶς''.<br>
'''βουκόλος, -ου (nom commun) (m)''' : bouvier.<br>
'''βουλευτήριον, -ίου (nom commun) (n)''' : bouleutérion.<br>
'''βουλή, -ῆς (nom commun) (f)''' : Volonté, vouloir. Décision, conseil. Sénat athénien.<br>
'''βούλησις, -ήσεως (nom commun) (f)''' : volonté.<br>
'''βούπρηστις, -ήστιδος (nom commun) (f)''' : bupreste.<br>
'''βουνός, -οῦ (nom commun) (m)''' : mont, montagne.<br>
'''βοῦς, -ός (nom commun) (m/f)''' : bœuf, vache.<br>
'''βουστροφηδόν (adverbe)''' : En écrivant alternativement de gauche à droite, puis de droite à gauche.<br>
'''βούτημα, -ήματος (nom commun) (n)''' : biscuit.<br>
'''βούτυρον, -ύρου (nom commun) (n)''' : beurre.<br>
'''βραδινός, -ή -όν (adjectif)''' : Forme éolienne de ''ῥαδινός''.<br>
'''βραϐευτής, -οῦ (nom commun) (m)''' : arbitre.<br>
'''βραδέως (adverbe)''' : lentement.<br>
'''βραδύς, -εῖα, -ύ (adjectif)''' : lent.<br>
'''βραδύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''βραδύς''.<br>
'''βραδύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''βραδύς''.<br>
'''βραδυτής, -ῆτος (nom commun) (f)''' : lenteur.<br>
'''βράγος, -ους (nom commun) (n)''' : bas-fond.<br>
'''βραχέως (adverbe)''' : courtement.<br>
'''βράχος, -ου (nom commun) (m)''' : écueil.<br>
'''βραχύς, -εῖα, -ύ (adjectif)''' : court.<br>
'''βραχύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''βραχύς''.<br>
'''βραχύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''βραχύς''.<br>
'''βρέμω (verbe)''' : gronder, retentir.<br>
'''βρένθος, -ου (nom commun) (m)''' : fierté.<br>
'''βρέφος, -ους (nom commun) (n)''' : fœtus, nouveau-né.<br>
'''βρεχμός, -οῦ (nom commun) (m)''' : .<br>
'''βρία, -ης (nom commun) (f)''' : ville.<br>
'''βρίμημα, -ήματος (nom commun) (n)''' : .<br>
'''βρεττανικός, -ή, -όν (adjectif)''' : breton insulaire.<br>
'''βρεττανικῶς (adverbe)''' : en breton insulaire.<br>
'''βρεττανικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''βρεττανικός''.<br>
'''βρεττανικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''βρεττανικός''.<br>
'''βρεττανικώτατα, -, - (adverbe)''' : Superlatif de ''βρεττανικῶς''.<br>
'''βρεττανικώτερον, -, - (adverbe)''' : Comparatif de ''βρεττανικῶς''.<br>
'''βρόμος, -ου (nom commun) (m)''' : Frémissement, grondement. Pétillement du feu. Grondement du tonnerre.<br>
'''βροντή, -ῆς (nom commun) (f)''' : Tonnerre. Stupeur.<br>
'''βροχίς, -δος (nom commun) (f)''' : .<br>
'''βρόχος, -ου (nom commun) (m)''' : nœud coulant.<br>
'''βρυγμός, -οῦ (nom commun) (m)''' : bruxisme.<br>
'''βρύον, -ου (nom commun) (n)''' : mousse (plante).<br>
'''βρυχηθμός, -οῦ (nom commun) (m)''' : rugissement.<br>
'''βρυχός, -οῦ (nom commun) (m)''' : brycose.<br>
'''βρυχῶμαι (verbe)''' : rugir.<br>
'''βύας, -ου (nom commun) (m)''' : hibou.<br>
'''βύρσα, -ας (nom commun) (f)''' : outre, cuir.<br>
'''βρώσιμος, -ος, -ον (adjectif)''' : mangeable.<br>
'''βρῶσις, -ώσεως (nom commun) (f)''' outre, cuir.<br>
'''βωϐός, -ή -όν (adjectif)''' : muet.<br>
'''βωλά, -ᾶς (nom commun) (f)''' : Forme dorienne de ''βουλή''.<br>
'''Βάαλ (nom propre) (m)''' : Baal.<br>
'''Βαϐυλωνεύς, -έως (nom commun) (m)''' : Babylonien.<br>
'''Βαϐυλωνία, -ας (nom commun) (f)''' : Babylonie.<br>
'''Βαϐυλωνιακός, -ός, -όν (adjectif)''' : babylonien.<br>
'''Βαϐυλώνιος, -ος, -ον (adjectif)''' : babylonien.<br>
'''Βαϐυλωνίς, -δος (nom commun) (f)''' : Babylonienne.<br>
'''Βαϐυλών, -ῶνος (nom propre) (f)''' : Babylone.<br>
'''Βάκχος, -ου (nom propre) (m)''' : Bacchus. (Épithète de Dionysos, parfois de Zeus.)<br>
'''Βαλλά, -ᾶς (nom propre) (f)''' : Bilha.<br>
'''Βαρϐάρα, -ας (nom propre) (f)''' : Barbara.<br>
'''Βαρϐαρικόν, -οῦ (nom propre) (m)''' : Barbaricum.<br>
'''Βαρθολομαῖος, -ίου (nom propre) (m)''' : Barthélémy.<br>
'''Βασίλειος, -ίου (nom propre) (m)''' : Basile.<br>
'''Βαταυΐα, -ας (nom propre) (f)''' : Batavie.<br>
'''Βατραχομυομαχία, -ας (nom propre) (f)''' : Bataille des grenouilles et des rats. (Parodie de l’''Iliade'' attribuée à un dénommé Pigrès d’Halicarnasse par Plutarque.)<br>
'''Βάττος, -ου (nom propre) (m)''' : Battos.<br>
'''Βαυϐώ, -οῦς (nom propre) (f)''' : Baubo.<br>
'''Βαῦκος, -ύκου (nom propre) (m)''' : Baukos.<br>
'''Βεελφεγώρ (nom propre) (m)''' : Belphégor.<br>
'''Βενδῖς, -ίδος (nom propre) (f)''' : Bendis.<br>
'''Βερενίκη, -ης (nom propre) (f)''' : Bérénice.<br>
'''Βηθανία, -ας (nom propre) (f)''' : Béthanie.<br>
'''Βηθλεέμ (nom propre) (f)''' : Bethléem.<br>
'''Bηλησαμα, -ας (nom propre) (f)''' : Belisama.<br>
'''Βῆλος, -ήλου (nom propre) (m)''' : Bel.<br>
'''Βηρυτός, -οῦ (nom propre) (f)''' : Beyrouth.<br>
'''Βία, -ας (nom propre) (m)''' : Bia.<br>
'''Βλάσιος, -ίου (nom propre) (m)''' : Blaise.<br>
'''Βλάχος, -ου (nom commun) (m)''' : Valaque.<br>
'''Βοανεργές (nom propre) (m)''' : Boanergès.<br>
'''Βοιωτία, -ας (nom propre) (f)''' : Béotie. (Région de Grèce centrale.)<br>
'''Βοιωτός, -οῦ (nom commun) (m)''' : Béotien.<br>
'''Βοιωτίς, -δος (nom commun) (f)''' : Béotienne.<br>
'''Βορέας, -ου (nom propre) (m)''' : Borée. (dieu du vent du Nord)<br>
'''Βορέης, -ου (nom propre) (m)''' : Forme ionienne de ''Βορέας''.<br>
'''Βορρᾶς, -ᾶ (nom propre) (m)''' : Forme attique de ''Βορέας''.<br>
'''Βοσπορίτης, -ου (nom commun) (m)''' : Bosphorite.<br>
'''Βόσπορος, -όρου (nom propre) (m)''' : Bosphore.<br>
'''Βούϐαστις, -άστιος (nom propre) (f)''' : Bastet.<br>
'''Βουκέφαλος, -άλου (nom propre) (m)''' : Bucéphale (Cheval préféré d’Alexandre le Grand.)<br>
'''Βουτώ, -οῦς (nom propre) (f)''' : Ouadjet.<br>
'''Βρέννος, -ου (nom commun) (m)''' : Brennos.<br>
'''Βρεττανία, -ας (nom propre) (f)''' : Bretagne (province romaine).<br>
'''Βρεττανίς, -δος (nom commun) (f)''' : Bretonne insulaire.<br>
'''Βρεττανός, -οῦ (nom commun) (m)''' : Breton insulaire.<br>
'''Βύϐλος, -ου (nom commun) (f)''' : Byblos.<br>
'''Βυζάντιον, -ίου (nom propre) (n)''' : Byzance.<br>
'''Βύζας, -αντος (nom propre) (m)''' : Byzas.<br>
'''Βιθυνία, -ας (nom propre) (f)''' : Bithynie.<br>
'''Βιθυνίς, -δος (nom commun) (f)''' : Bithynienne.<br>
'''Βιθυνός, -οῦ (nom commun) (o)''' : Bithynien.<br>
==Γ==
'''γα (particule)''' : Forme dorienne et béotienne de ''γε''.<br>
'''γαίω (verbe)''' : exulter, se réjouir.<br>
'''γάλα, -κτος (nom commun) (n)''' : lait.<br>
'''γαλανός, -ός, -όν (adjectif)''' : Forme dorienne de ''γαληνός''.<br>
'''γαλήνη, -ης, -ης (nom commun) (f)''' : Calme de la mer. (Par extension) Calme, sérénité. Galène. Antidote contre les morsures de vipères.<br>
'''γαληνός, -ός, -όν (adjectif)''' : calme (spécialement à propos de la mer).<br>
'''γαλέη, -ης (nom commun) (f)''' : belette.<br>
'''γαλῆ, -ς (nom commun) (f)''' : Forme alternative de ''γαλέη''.<br>
'''γαλιῶ (verbe)''' : Être lascif comme une belette.<br>
'''γάμμα (nom commun) (n)''' : gamma.<br>
'''γαμέτης, -ου (nom commun) (m)''' : époux, mari.<br>
'''γαμῶ (verbe)''' : se marier (quand on parle d’un homme)<br>
'''γάργαλος, -άλου (nom commun) (m)''' : .<br>
'''γαργαλίζω (verbe)''' : chatouiller.<br>
'''γαργαλισμός, -οῦ (nom commun) (m)''' : chatouille.<br>
'''γαργαρεών, -ῶνος (nom commun) (m)''' : luette.<br>
'''γάρ (conjonction)''' : car ; en effet.<br>
'''γαστήρ, -τρός (nom commun) (f)''' : ventre.<br>
'''γαῦρος, -ύρη, -ῦρον (adjectif)''' : Exultant, joyeux. Hautain, dédaigneux.<br>
'''γαυρότατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''γαῦρος''.<br>
'''γαυρότερος, -έρα, -ερον (adjectif)''' : Comparatif de ''γαῦρος''.<br>
'''γαῦρως (adverbe)''' : joyeusement ; hautainement, dédaigneusement.<br>
'''γαυρότης, -τος (nom commun) (f)''' : Exultation. Emportement, férocité.<br>
'''γε (particule)''' : (Devient ''γ’'' devant un mot commençant par une voyelle.) Marque une restriction, une affirmation, ou une conclusion.<br>
'''γείτων, -ονος (nom commun) (m/f)''' : voisin(e).<br>
'''γέλαιμι (verbe)''' : Forme éolienne de ''γελάω''.<br>
'''γελάω (verbe)''' : Rire ; briller.<br>
'''γελοῖος, -ία, -ῖον (adjectif)''' : ridicule ; risible.<br>
'''γελοιότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''γελοῖος''.<br>
'''γελοιότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''γελοῖος''.<br>
'''γελοίως (adverbe)''' : ridiculement ; risiblement.<br>
'''γελόω (verbe)''' : Forme homérique de ''γελάω''.<br>
'''γέλως, -τος (nom commun) (m)''' : rire.<br>
'''γελωτοποιός, -ός, -όν (adjectif)''' : comique.<br>
'''γελωτοποιός, -οῦ (nom commun) (m)''' : Bouffon ; pitre.<br>
'''γεμίζω (verbe)''' : emplir.<br>
'''γέμισμα, -ίσματος (nom commun) (n)''' : emplissage.<br>
'''γενεά, -ᾶς (nom commun) (f)''' : naissance, genre ; espèce.<br>
'''γένεσις, -έσεως (nom commun) (f)''' : origine, source. Naissance. Création.<br>
'''γενετήσιος, -α, -ον (adjectif)''' : sexuel.<br>
'''γενετησιότατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''γενετήσιος''.<br>
'''γενετησιότερος, -έρα, -ερον (adjectif)''' : Comparatif de ''γενετήσιος''.<br>
'''γενετησίως (adverbe)''' : sexuellement.<br>
'''γενναῖος, -ία, -ῖον (adjectif)''' : vaillant.<br>
'''γενναιότατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''γενναῖος''.<br>
'''γενναιότερος, -έρα, -ερον (adjectif)''' : Comparatif de ''γενναῖος''.<br>
'''γενναίως (adverbe)''' : vaillamment.<br>
'''γενναιότης, -ητος (nom commun) (f)''' : vaillance.<br>
'''γεννητικός, -η, -ον (adjectif)''' : génital.<br>
'''γένος, -ους (nom commun) (n)''' : naissance, origine, descendance ; race, genre, espèce ; classe, corporation; nation, peuple, tribu.<br>
'''γεννῶ (verbe)''' : accoucher.<br>
'''γερόντειος, -α, -ον (adjectif)''' : .<br>
'''γεροντεύω (verbe)''' : .<br>'''γέρανος, -άνου (nom commun) (m/f)''' : grue (oiseau).<br>
'''γερουσία, -ας (nom commun) (f)''' : sénat.<br>
'''γερουσιάρχης, -ου (nom commun) (m)''' : président du sénat.<br>
'''γερουσιαστής, -οῦ (nom commun) (m)''' : sénateur.<br>
'''γέρων, -οντος (nom commun) (m)''' : vieillard.<br>
'''γεῦμα, -ύματος (nom commun) (n)''' : déjeuner.<br>
'''γεῦσις, -ύσεως (nom commun) (f)''' : goût.<br>
'''γεύω (verbe)''' : goûter.<br>
'''γέφυρα, -ύρας (nom commun) (f)''' : Chaussée. Pont.<br>
'''γεω- (préfixe)''' : relatif à la terre.<br>
'''γῆ, -ς (nom commun) (f)''' : terre.<br>
'''γήινος, -η, -ο (adjectif)''' : terrestre.<br>
'''γήρανσις, -άνσεως (nom commun) (f)''' : .<br>
'''γηράσκω (verbe)''' : .<br>
'''γῆρας, -ήρως (nom commun) (n)''' : vieillesse.<br>
'''γῆρυς, -ήρυος (nom commun) (f)''' : Voix ; discours.<br>
'''γηρύω (verbe)''' : chanter.<br>
'''γιγαντιαῖος, -ία, -ῖον (adjectif)''' : gigantesque.<br>
'''γιγαντιότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''γιγαντιαῖος''.<br>
'''γιγαντιότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''γιγαντιαῖος''.<br>
'''γιγαντίως (adverbe)''' : gigantesquement.<br>
''', -, - (adverbe)''' : Superlatif de ''γιγαντίως''.<br>
''', -, - (adverbe)''' : Comparatif de ''γιγαντίως''.<br>
'''γίγας, -αντος (nom commun) (m)''' : géant.<br>
'''γίγνομαι (verbe)''' : engendrer.<br>
'''γίνιουμαι (verbe)''' : Forme béotienne de ''γίγνομαι''.<br>
'''γίνομαι (verbe)''' : Forme ionienne de ''γίγνομαι''.<br>
'''γίνυμαι (verbe)''' : Forme thessalienne de ''γίγνομαι''.<br>
'''γιγνώσκω (verbe)''' : Apprendre à connaître.<br>
'''γινώσκω (verbe)''' : Forme ionienne de ''γιγνώσκω''.<br>
'''γλάγος, -ους (nom commun) (n)''' : Forme poétique de ''γάλα''.<br>
'''γλάσσα, -ης (nom commun) (f)''' : Forme ionienne de ''γλῶσσα''.<br>
'''γλαυκός, -ή, -όν (adjectif)''' : Brillant, étincelant, éclatant. D’un vert pâle ou gris.<br>
'''γλαυκότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''γλαυκός''.<br>
'''γλαυκότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''γλαυκός''.<br>
'''γλαυκῶς (adverbe)''' : vivement.<br>
'''γλαυκώτατα, -, - (adverbe)''' : Superlatif de ''γλαυκῶς''.<br>
'''γλαυκώτερον, -, - (adverbe)''' : Comparatif de ''γλαυκῶς''.<br>
'''γλαῦξ, -κός (nom commun) (f)''' : chouette.<br>
'''γλαύσσω (verbe)''' : briller (En parlant des yeux).<br>
'''γλεῦκος, -ύκους (nom commun) (n)''' : moût.<br>
'''γλέφαρον, -άρου (nom commun) (n)''' : Forme dorienne de ''βλέφαρον''.<br>
'''γλήνη, -ης (nom commun) (f)''' : pupille (partie de l’œil).<br>
'''γλῆνος, -ήνους (nom commun) (n)''' : splendeur.<br>
'''γλουτιαῖος, -ία, -αῖν (adjectif)''' : glutéal.<br>
'''γλουτός, -οῦ (nom commun) (m)''' : derrière ; fesse.<br>
'''γλυκέως (adverbe)''' : doucement.<br>
'''γλυκερός, -ή, -όν (adjectif)''' : doux.<br>
'''γλυκερῶς (adverbe)''' : doucement.<br>
'''γλυκύς, -εῖα, -ύ (adjectif)''' : doux, sucré.<br>
'''γλυκύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''γλυκύς''.<br>
'''γλυκύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''γλυκύς''.<br>
'''γλυκώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''γλυκερός''.<br>
'''γλυκώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''γλυκερός''.<br>
'''γλυπτόν, -οῦ (nom commun) (n)''' : .<br>
'''γλώνη, -ης (nom commun) (f)''' : poupée.<br>
'''γλῶσσα, -ώσσης (nom commun) (f)''' : langue. (Organe buccal ; expression orale)<br>
'''γλωσσίς, -δος (nom commun) (f)''' : glotte.<br>
'''γλῶττα, -ώττης (nom commun) (f)''' : Forme attique de ''γλῶσσα''.<br>
'''γλωττίς, -δος (nom commun) (f)''' : Forme attique de ''γλωσσίς''.<br>
'''γνάθος, -ου (nom commun) (f)''' : mâchoire.<br>
'''γνήσιος, -α, -ον (adjectif)''' : véritable.<br>
'''γνῶσις, -ώσεως (nom commun) (f)''' : Savoir ; connaissance, notion. Reconnaissance ; enquête, instruction judiciaire.<br>
'''γοάω (verbe)''' : se lamenter.<br>
'''γόγγρος, -ου (nom commun) (m)''' : congre.<br>
'''γόησσα, -ας (nom commun) (f)''' : enchanteresse ; magicienne.<br>
'''γόης, -τος (nom commun) (m)''' : enchanteur ; magicien.<br>
'''γονεύς, -έως (nom commun) (m)''' : père (parent).<br>
'''γονή, -ῆς (nom commun) (f)''' : génération ; procréation.<br>
'''γονόρροια, -ας (nom commun) (f)''' : gonorrhée.<br>
'''γόνος, -ου (nom commun) (m)''' : procréation.<br>
'''γόνυ, -ατος (nom commun) (n)''' : genou.<br>
'''γόος, -ου (nom commun) (m)''' : lamentation.<br>
'''γοργός, -ή, -όν (adjectif)''' : terrible.<br>
'''γούνα, -ας (nom commun) (f)''' : .<br>
'''γοῦνος, -ύνου (nom commun) (m)''' : Forme ionienne de ''γόνος''.<br>
'''γοῶ (verbe)''' : enchanter, ensorceler.<br>
'''γράθμα, -τος (nom commun) (n)''' : Forme dorienne de ''γράμμα''.<br>
'''γραῖα, -ίας (nom commun) (f)''' : vieillarde.<br>
'''γραμματεύς, -έως (nom commun) (m)''' : scribe.<br>
'''γράμμα, -τος (nom commun) (n)''' : Caractère gravé. Signes divers. Traits d’un dessin ou d’une peinture.<br>
'''γραπτός, -ή, -όν (adjectif)''' : écrit.<br>
'''γραπτότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''γραπτός''.<br>
'''γραπτότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''γραπτός''.<br>
'''γραπτῶς (adverbe)''' : .<br>
'''γραῦς, -ός (nom commun) (f)''' : vieillarde.<br>
'''γραφεύς, -έως (nom commun) (m)''' : peintre.<br>
'''γραφή, -ῆς (nom commun) (f)''' : peinture.<br>
'''γραφία, -ας (nom commun) (f)''' : écriture.<br>
'''-γραφία, -ας (suffixe)''' : relatif à l’écriture.<br>
'''γραφικός, -ή, -όν (adjectif)''' : peint.<br>
'''γραφικῶς (adverbe)''' : graphiquement.<br>
'''γραφικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''γραφικός''.<br>
'''γραφικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''γραφικός''.<br>
'''γραφίς, -δος (nom commun) (f)''' : .<br>
'''γράφω (verbe)''' : écrire.<br>
'''γρηγοράς, -δος (nom commun) (f)''' : vivacité.<br>
'''γρήγορος, -η, -ον (adjectif)''' : vif.<br>
'''γρηγόρως (adverbe)''' : vivement.<br>
'''γρηγορώτατα, -, - (adverbe)''' : Superlatif de ''γρηγόρως''.<br>
'''γρηγορώτερον, -, - (adverbe)''' : Comparatif de ''γρηγόρως''.<br>
'''γρηγορώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''γρήγορος''.<br>
'''γρηγορώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''γρήγορος''.<br>
'''γρῖφος, -ίφου (nom commun) (m)''' : filet ; énigme.<br>
'''γρόνθος, -ου (nom commun) (m)''' : poing.<br>
'''γρύλλος, -ου (nom commun) (m)''' : grillon.<br>
'''γρύψ, -πός (nom commun) (m)''' : griffon.<br>
'''γυμνάζω (verbe)''' : entraîner (diriger l’exercice sportif).<br>
'''γυμνάσιον, -ίου (nom commun) (n)''' : Lieu public réservé aux exercices corporels.<br>
'''γυμνός, -ή, -όν (adjectif)''' : Nu ; légèrement vêtu.<br>
'''γυμνότατα, -, - (adverbe)''' : Superlatif de ''γυμνῶς''.<br>
'''γυμνότερον, -, - (adverbe)''' : Comparatif de ''γυμνῶς''.<br>
'''γυμνότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''γυμνός''.<br>
'''γυμνότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''γυμνός''.<br>
'''γυμνότης, -τος (nom commun) (f)''' : nudité.<br>
'''γύμνωσις, -ώσεως (nom commun) (f)''' : dénudage.<br>
'''γυμνῶς (adverbe)''' : .<br>
'''γυμνῶ (verbe)''' : dénuder.<br>
'''γυνά, -ᾶς (nom commun) (f)''' : Forme dorienne de ''γυνή''.<br>
'''γυναικᾶς, - (nom commun) (m)''' : homme à femmes.<br>
'''γυναικεῖον, -ίου (nom commun) (n)''' : Appartement réservé aux femmes.<br>
'''γυναικεῖος, -ία, -ῖον (adjectif)''' : féminin.<br>
'''γυναικειότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''γυναικεῖος''.<br>
'''γυναικειότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''γυναικεῖος''.<br>
'''γυναικείως (adverbe)''' : fémininement.<br>
'''γυναικομανία, -ας (nom commun) (f)''' : gynécomanie.<br>
'''γυνή, -αικός (nom commun) (f)''' : Femme, épouse ; femelle des animaux.<br>
'''γυρῖνος, -ίνου (nom commun) (m)''' : têtard.<br>
'''γῦρος, -ύρου (nom commun) (m)''' : Anneau ; cercle.<br>
'''γυρός, -οῦ (nom commun) (m)''' : rond.<br>
'''γύψ, -πος (nom commun) (m)''' : vautour.<br>
'''γωνία, -ας (nom commun) (f)''' : Angle ; coin.<br>
'''γωρυτός, -οῦ (nom commun) (m)''' : carquois.<br>
'''Γαϐριήλ (nom propre) (m)''' : Gabriel.<br>
'''Γάζα, -ης (nom commun) (f)''' : Gaza.<br>
'''Γαῖα, -ίας (nom propre) (f)''' : [[wikt:Gaïa|Gaïa]].<br>
'''Γαῖη, -ίης (nom propre) (f)''' : Forme ionienne de ''Γαῖα''.<br>
'''Γαλινθιάς, -δος (nom propre) (f)''' : Galanthis.<br>
'''Γαλάτεια, -ίας (nom propre) (f)''' : Galatée (Néréide).<br>
'''Γαλατεία, -ας (nom propre) (f)''' : Galatée.<br>
'''Γαλάτης, -ου (nom commun) (m)''' : Galate.<br>
'''Γαλατία, -ας (nom propre) (f)''' : Galatie.<br>
'''Γαλλία, -ας (nom propre) (f)''' : Gaule.<br>
'''Γανυμήδης, -ου (nom propre) (m)''' : Ganymède.<br>
'''Γεδεών, -ος (nom propre) (m)''' : Gédéon.<br>
'''Γελλώ, -οῦς (nom propre) (f)''' : Gello.<br>
'''Γέλων, -ος (nom propre) (m)''' : Gélon.<br>
'''Γεννησαρέτ (nom propre) (f)''' : Gennésaret.<br>
'''Γερμανία, -ας (nom propre) (f)''' : Allemagne.<br>
'''Γερμανίς, -δος (nom commun) (f)''' : Allemande.<br>
'''Γερμανός, -οῦ (nom commun) (m)''' : Allemand.<br>
'''Γέτης, -ου (nom commun) (m)''' : Gète.<br>
'''Γεώργιος, -ίου (prénom) (m)''' : Georges.<br>
'''Γῆρας, -ήρως (nom propre) (m)''' : Géras.<br>
'''Γηρυόνης, -ου (nom propre) (m)''' : Forme alternative de ''Γηρυών''.<br>
'''Γηρυών, -όνος (nom propre) (m)''' : Géryon.<br>
'''Γῆ, -ς (nom propre) (f)''' : Terre.<br>
'''Γλαῦκος, -ύκου (nom propre) (m)''' : Glaucos.<br>
'''Γοργώ, -όνος (nom propre) (f)''' : Gorgone.<br>
'''Γότθος, -ου (nom commun) (m)''' : Goth.<br>
'''Γοῦτος, -ύτου (nom commun) (m)''' : Geat.<br>
'''Γραῖα, -ίας (nom propre) (f)''' : Grée.<br>
'''Γρηγόριος, -ίου (nom propre) (m)''' : Grégoire.<br>
'''Γύγης, -ου (nom propre) (m)''' : Gygès.<br>
'''Γύθειον, -ίου (nom propre) (n)''' : Gythio.<br>
'''Γύθιον, -ίου (nom propre) (n)''' : Forme alternative de ''Γύθειον''.<br>
==Δ==
'''δάγυς, -ύδος (nom commun) (f)''' : dagyde.<br>
'''δαήρ, -έρος (nom commun) (m)''' : beau-frère.<br>
'''δαιμόνιος, -ία, -όνιον (adjectif)''' : étrange.<br>
'''δαιμόνιον, -ίου (nom commun) (n)''' : génie (être merveilleux).<br>
'''δαιμονίως (adverbe)''' : étrangement.<br>
'''δαιμονίοτατα, -, - (adverbe)''' : Superlatif de ''δαιμονίως''.<br>
'''δαιμονίοτερον, -, - (adverbe)''' : Comparatif de ''δαιμονίως''.<br>
'''δαιμονιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''δαιμόνιος''.<br>
'''δαιμονιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''δαιμόνιος''.<br>
'''δαίμων, -ονος (nom commun) (m)''' : divinité.<br>
'''δακκύλιος, -υλίου (nom commun) (m)''' : Forme béotienne de ''δάκτυλος''.<br>
'''δάκνω (verbe)''' : mordre.<br>
'''δάκος, -ους (nom commun) (n)''' : .<br>
'''δάκρυ, -ύου (nom commun) (n)''' : larme.<br>
'''δακτύλιον, -ίου (nom commun) (n)''' : anneau.<br>
'''δάκτυλος, -ύλου (nom commun) (m)''' : doigt.<br>
'''δάλτος, -ου (nom commun) (f)''' : Forme chypriote de ''δέλτος''.<br>
'''δαμάζω (dompter)''' : domestiquer ; dompter.<br>
'''δάμαλις, -άλεως (nom commun) (f)''' : génisse.<br>
'''δαμοκρατία, -ας (nom commun) (f)''' : Forme dorienne de ''δημοκρατία''.<br>
'''δᾶμος, -άμου (nom commun) (m)''' : Forme dorienne de ''δῆμος''.<br>
'''δάνειον, -ίου (nom commun) (n)''' : prêt.<br>
'''δάνος, -ου (nom commun) (m)''' : Forme macédonienne de ''θάνατος''.<br>
'''δαρθάνω (verbe)''' : s’endormir.<br>
'''δάσος, -ους (nom commun) (n)''' : bois (lieu), forêt.<br>
'''δασύνω (verbe)''' : .<br>
'''δασύς, -εῖα, -ύ (adjectif)''' : Velu, poilu. feuillu ; boisé.<br>
'''δασύτης, -τος (nom commun) (f)''' : pilosité.<br>
'''δαῦκον, -ύκου (nom commun) (n)''' : carotte ou navet utilisé en médecine.<br>
'''δεῖγμα, -ίγματος (nom commun) (f)''' : échantillon.<br>
'''δείδω (verbe)''' : avoir peur.<br>
'''δείκτης, -ου (nom commun) (m)''' : index.<br>
'''δεινός, -ή, -όν (adjectif)''' : terrible.<br>
'''δέ (particule)''' : mais, puis, d’autre part, donc.<br>
'''δέκα (adjectif numéral)''' : dix.<br>
'''δεκαετία, -ας (nom commun) (f)''' : décennie.<br>
'''δεκάς, -δος (nom commun) (f)''' : dizaine.<br>
'''δέκομαι (verbe)''' : Forme éolienne et ionienne de ''δέχομαι''.<br>
'''δελεάζω (verbe)''' : appâter.<br>
'''δέλεαρ, -τος (nom commun) (n)''' : appât.<br>
'''δέλτα (nom commun) (n)''' : delta.<br>
'''δέλτος, -ου (nom commun) (f)''' : tablette d’écriture.<br>
'''δέλφαξ, -κος (nom commun) (f)''' : .<br>
'''δελφίς, -ῖνος (nom commun) (m)''' : dauphin.<br>
'''δελφύς, -ος (nom commun) (f)''' : matrice.<br>
'''δέμω (verbe)''' : construire.<br>
'''δενδρολίϐανον, -άνου (nom commun) (n)''' : romarin.<br>
'''δένδρον, -ου (nom commun) (n)''' : arbre.<br>
'''δένδρεον, -ου (nom commun) (n)''' : Forme homérique de ''δένδρον''.<br>
'''δεξιτερός, -ή, -όν (adjectif)''' : Forme de ''δεξιός''.<br>
'''δεξιός, -ά, -όν (adjectif)''' : qui est à droite, placé à droite. (Par suite) De bon augure, favorable. Qui a de la dextérité, adroit, industrieux, habile.<br>
'''δέος, -ους (nom commun) (n)''' : effroi, peur.<br>
'''δέρας, -ατος (nom commun) (n)''' : peau, cuir.<br>
'''δέρκομαι (verbe)''' : voir clair.<br>
'''δέρμα, -τος (nom commun) (n)''' : peau.<br>
'''δέρω (verbe)''' : écorcher.<br>
'''δέσμευσις, -ύσεως (nom commun) (f)''' : lien.<br>
'''δεσμεύω (verbe)''' : lier.<br>
'''δέσμη, -ης (nom commun) (f)''' : bouquet, faisceau, liasse.
'''δεσμός, -οῦ (nom commun) (m)''' : Lien (corde, câble, amarre, courroie, nœud). (Par extension) Clou. (D’ordinaire au pluriel) Liens, chaînes, fers. (Par suite) Emprisonnement, prison. (En général) Captivité. (Figuré) Liens d’amitié.<br>
'''δέσποινα, -ίνης (nom commun) (f)''' : maîtresse d’une maisonnée.<br>
'''δεσπότης, -ου (nom commun) (m)''' : maître d’une maisonnée ; maître d’un dème.<br>
'''δεῦμα, -ύματος (nom commun) (n)''' : chair cuite.<br>
'''δεῦρο (adverbe)''' : ici (avec mouvement).<br>
'''δευτεραγωνιστής, -οῦ (nom commun) (m)''' : deutéragoniste.<br>
'''δεύτερος, -α, -ον (adjectif)''' : deuxième.<br>
'''δευτήρ, -ῆρος (nom commun) (m)''' : chaudron.<br>
'''δεύω (verbe)''' : mouiller.<br>
'''δέφυρα, -ύρας (nom commun) (f)''' : Forme crétoise de ''γέφυρα''.<br>
'''δέχομαι (verbe)''' : Accepter, admettre, agréer. Accueillir, recevoir, recueillir, adopter. Prendre, revêtir, comporter.<br>
'''δή (particule)''' : vraiment, assurément.<br>
'''δῆγμα, -ήγματος (nom commun) (m)''' : morsure.<br>
'''δηκτήριος, -ος, -ον (adjectif)''' : mordant.<br>
'''δηλητήρ, -ρός (nom commun) (m)''' : destructeur.<br>
'''δηλοῦμαι (verbe)''' : détruire.<br>
'''δήλωσις, -ώσεως (nom commun) (f)''' : déclaration.<br>
'''δηλῶ (verbe)''' : déclarer.<br>
'''δημοκρατία, -ας (nom commun) (f)''' : république.<br>
'''δημόσιος, -ία, -όσιον (adjectif)''' : public.<br>
'''δημοσίως (adverbe)''' : publiquement.<br>
'''δημοσιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''δημόσιος''.<br>
'''δημοσιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''δημόσιος''.<br>
'''δῆμος, -ήμου (nom commun) (m)''' : contrée, pays, terre.<br>
'''δημότης, -ου (nom commun) (m)''' : concitoyen.<br>
'''δημοτικός, -ή, -όν (adjectif)''' : commun.<br>
'''δημοτικῶς (adverbe)''' : communément.<br>
'''δημοτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''δημοτικός''.<br>
'''δημοτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''δημοτικός''.<br>
'''δημοτικώτατα, -, - (adverbe)''' : Superlatif de ''δημοτικῶς''.<br>
'''δημοτικώτερον, -, - (adverbe)''' : Comparatif de ''δημοτικῶς''.<br>
'''δημωδέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''δημώδης''.<br>
'''δημωδέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''δημώδης''.<br>
'''δημώδης, -ης, -ες (adjectif)''' : vernaculaire.<br>
'''δημωδῶς (adverbe)''' : vernaculairement.<br>
'''δήν (particule)''' : il y a longtemps.<br>
'''δηρός, -ά, -όν (adjectif)''' : .<br>
'''διά (adverbe ; préposition)''' : .<br>
'''διαϐεϐαιῶ (verbe)''' : garantir (se rendre garant de l’existence de la réalité d’une chose).<br>
'''διαϐιϐάζω (verbe)''' : lire.<br>
'''διαϐιϐρώσκω (verbe)''' : .<br>
'''διάϐολος, -ου (masculin) (m)''' : calomniateur ; diable.<br>
'''διαϐολικός, -ή, -όν (adjectif)''' : du diable.<br>
'''διαϐολικῶς (adverbe)''' : diaboliquement.<br>
'''διαϐολικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''διαϐολικός''.<br>
'''διαϐολικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''διαϐολικός''.<br>
'''διάγνωσις, -ώσεως (nom commun) (f)''' : .<br>
'''διάδημα, -ήματος (nom commun) (n)''' : diadème.<br>
'''διαδῶ (verbe)''' : .<br>
'''διάζωμα, -ώματος (nom commun) (n)''' : caleçon.<br>
'''διάθεσις, -έσεως (nom commun) (f)''' : disposition.<br>
'''διαθήκη, -ης (nom commun) (f)''' : testament, volonté (document écrit). Testament (livre religieux)<br>
'''διαίρεσις, -έσεως (nom commun) (f)''' : division.<br>
'''διαίσθησις, -ήσεως (nom commun) (f)''' : intuition.<br>
'''διαισθητικός, -ή, -όν (adjectif)''' : intuitif.<br>
'''διαισθητικῶς (adverbe)''' : intuitivement.<br>
'''διαιτητής, -οῦ (nom commun) (m)''' : arbitre.<br>
'''διακινῶ (verbe)''' : .<br>
'''διάκονος, -όνου (nom commun) (m/f)''' : Serviteur, servante.<br>
'''διακοπή, -ῆς (nom commun) (f)''' : interruption.<br>
'''διακόπτω (verbe)''' : interrompre.<br>
'''διακορεύω (verbe)''' : déflorer.<br>
'''διακόρησις, -ήσεως (nom commun) (f)''' : défloration.<br>
'''διάκρισις, -ίσεως (nom commun) (f)''' : discrétion ; distinction.<br>
'''διακριτικός, -ή, -όν (adjectif)''' : discret ; distinct.<br>
'''διακριτικότατα, -, - (adverbe)''' : Superlatif de ''διακριτικῶς''.<br>
'''διακριτικότερον, -, - (adverbe)''' : Comparatif de ''διακριτικῶς''.<br>
'''διακριτικότατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''διακριτικός''.<br>
'''διακριτικότερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''διακριτικός''.<br>
'''διακριτικῶς (adverbe)''' : discrètement ; distinctement.<br>
'''διάλεξις, -έξεως (nom commun) (f)''' : exposé.<br>
'''διαλέγομαι (verbe)''' : discuter.<br>
'''διάλεκτος, -έκτου (nom commun) (f)''' : discussion ; dialecte.<br>
'''διαλλάττω (verbe)''' : transiger.<br>
'''διάλογος, -όγου (nom commun) (m)''' : dialogue.<br>
'''διαμαστίγωσις, -ώσεως (nom commun) (f)''' : .<br>
'''διαμαστιγῶ (verbe)''' : .<br>
'''διανόησις, -ήσεως (nom commun) (f)''' : pensée.<br>
'''διανοῶ (verbe)''' : penser.<br>
'''διαπραγμάτευσις, -ύσεως (nom commun) (f)''' : négociation.<br>
'''διαπραγματεύομαι (verbe)''' : négocier.<br>
'''διαρρήγνυμι (verbe)''' : cambrioler.<br>
'''διαρρήκτης, -ου (nom commun) (m)''' : cambrioleur.<br>
'''διάρρηξις, -ήξεως (nom commun) (m)''' : cambriolage.<br>
'''διάρροια, -ας (nom commun) (f)''' : diarrhée.<br>
'''διάσεισις, -ίσεως (nom commun) (f)''' : commotion.<br>
'''διασείω (verbe)''' : .<br>
'''διασκέδασις, -άσεως (nom commun) (f)''' : divertissement.<br>
'''διάστασις, -άσεως (nom commun) (f)''' : dimension.<br>
'''διαστέλλω (verbe)''' : Répandre, séparer. Distinguer, déterminer.<br>
'''διάστημα, -ήματος (nom commun) (n)''' : espace.<br>
'''διαστολή, -ῆς (nom commun) (f)''' : Élargissement, expansion, dilatation. Petite coche ou fente.<br>
(Figuré) Distinction.<br>
'''διάστρεμμα, -έμματος (nom commun) (n)''' : entorse.<br>
'''διαστρέφω (verbe)''' : disposer.<br>
'''διατίθημι (verbe)''' : disposer.<br>
'''διατριϐή, -ῆς (nom commun) (f)''' : conversation (philosophique).<br>
'''διαφθείρω (verbe)''' : corrompre.<br>
'''διαφθορά, -ᾶς (nom commun) (f)''' : corruption.<br>
'''διαφθορεῖον, -ίου (nom commun) (n)''' : .<br>
'''διαφθορεύς, -έως (nom commun) (m)''' : .<br>
'''διαχειριστής, -οῦ (masculin) (m)''' : gestionnaire.<br>
'''διαχείρισις, -ίσεως (nom commun) (f)''' : gestion.<br>
'''διαχειριστικός, -ή, -όν (adjectif)''' : .<br>
'''διαχειριστικῶς (adverbe)''' : -ment.<br>
'''διαχειριστικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''διαχειριστικός''.<br>
'''διαχειριστικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''διαχειριστικός''.<br>
'''διαχειριστικώτατα, -, - (adverbe)''' : Superlatif de ''διαχειριστικῶς''.<br>
'''διαχειριστικώτερον, -, - (adverbe)''' : Comparatif de ''διαχειριστικῶς''.<br>
'''διδάσκαλος, -άλου (nom commun) (m)''' : instituteur.<br>
'''διδάσκω (verbe)''' : enseigner, instruire ; entraîner.<br>
'''διαφέρω (verbe)''' : différer.<br>
'''διαφορά, -ᾶς (nom commun) (f)''' : différence.<br>
'''διαφορετικός, -ή, -όν (adjectif)''' : différent.<br>
'''διαφορετικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''διαφορετικός''.<br>
'''διαφορετικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''διαφορετικός''.<br>
'''διαφορετικώτατα, -, - (adverbe)''' : Superlatif de ''διαφορετικῶς''.<br>
'''διαφορετικώτερον, -, - (adverbe)''' : Comparatif de ''διαφορετικῶς''.<br>
'''δίδυμος, -ος, -ον (adjectif)''' : double ; jumeau.<br>
'''δίδωμι (verbe)''' : donner.<br>
'''διείσδυσις, -ύσεως (nom commun) (f)''' : pénétration.<br>
'''διεισδύω (verbe)''' : pénétrer.<br>
'''διένεξις, -έξεως (nom commun) (f)''' : contentieux.<br>
'''διεύθυνσις, -ύνσεως (nom commun) (f)''' : direction.<br>
'''διευθύνων, -ουσα, -ον (adjectif)''' : directeur.<br>
'''διευθυντής, -οῦ (nom commun) (m)''' : directeur.<br>
'''διευθύνω (verbe)''' : diriger.<br>
'''διθύραμϐος, -άμϐου (nom commun) (m)''' : dithyrambe.<br>
'''διήγημα, -ήματος (nom commun) (n)''' : conte.<br>
'''διήγησις, -ήσεως (nom commun) (f)''' : narration.<br>
'''διηγοῦμαι (verbe)''' : narrer.<br>
'''διήκονος, -όνου (nom commun) (m/f)''' : Forme ionienne de ''διάκονος''.<br>
'''διίσταμαι (verbe)''' : .<br>
'''διίστημι (verbe)''' : .<br>
'''δικάζω (verbe)''' : juger.<br>
'''δικαίωμα, -ώματος (nom commun) (n)''' : (Droit) Jugement. Justification. Décret.<br>
'''δικαιῶ (verbe)''' : rendre juste.<br>
'''δικαστήριον, -ίου (nom commun) (n)''' : tribunal.<br>
'''δικαστής, -οῦ (nom commun) (m)''' : juge.<br>
'''δίκη, -ης (nom commun) (f)''' : Coutume, manière, mode. Ordre, loi, droit. Justice. Jugement. Punition, vengeance, pénalité.<br>
'''δίκτυον, -ύου (nom commun) (n)''' : filet.<br>
'''δίνη, -ης (nom commun) (f)''' : tourbillon.<br>
'''δίλημμα, -ήμματος (nom commun) (n)''' : dilemme.<br>
'''διπλοῦς, -ῆ, -οῦν (adjectif)''' : double.<br>
'''διοίκησις, -ήσεως (nom commun) (f)''' : administration.<br>
'''διοικητής, -οῦ (nom commun) (m)''' : administrateur.<br>
'''διοικητικός, -ή, -όν (adjectif)''' : administratif.<br>
'''διοικῶ (verbe)''' : administrer.<br>
'''διορθώνω (verbe)''' : corriger.<br>
'''διουρητικόν, -οῦ (nom commun) (n)''' : diurétique.<br>
'''διουρητικός, -ή, -όν (adjectif)''' : diurétique.<br>
'''δισσός, -ή, -όν (adjectif)''' : double.<br>
'''δισσότατα, -, - (adverbe)''' : Superlatif de ''δισσῶς''.<br>
'''δισσότερον, -, - (adverbe)''' : Comparatif de ''δισσῶς''.<br>
'''δισσότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''δισσός''.<br>
'''δισσότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''δισσός''.<br>
'''δίς (adverbe)''' : deux fois.<br>
'''δισσῶς (adverbe)''' : doublement.<br>
'''δίφθογγος, -όγγου (nom commun) (f)''' : diphtongue.<br>
'''δίφουρα, -ύρας (nom commun) (f)''' : Forme laconienne de ''γέφυρα''.<br>
'''δίφρος, -ου (nom commun) (m)''' : tabouret.<br>
'''διχόνοια, -ας (nom commun) (f)''' : discorde.<br>
'''διχορεῖος, -ίου (nom commun) (m)''' : dichorée.<br>
'''δίψα, -ης (nom commun) (f)''' : soif.<br>
'''διψώ (verbe)''' : avoir soif.<br>
'''διωγμός, -οῦ (nom commun) (m)''' : persécution.<br>
'''διώκω (verbe)''' : persécuter.<br>
'''διῶρυξ, -ώρυγος (nom commun) (f)''' : canal.<br>
'''δμωή, -ῆς (nom commun) (f)''' : domestique, servante.<br>
'''δμῳή, -ῆς (nom commun) (f)''' : esclave.<br>
'''δμῴιος, -ίου (nom commun) (m)''' : esclave.<br>
'''δμώς, -ός (nom commun) (m)''' : domestique, serviteur.<br>
'''δόγμα, -τος (nom commun) (n)''' : Opinion. Décision, décret, arrêt. Doctrine.<br>
'''δογματικός, -ή, -όν (adjectif)''' : doctrinal.<br>
'''δογματικῶς (adverbe)''' : doctrinalement.<br>
'''δογματικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''δογματικός''.<br>
'''δογματικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''δογματικός''.<br>
'''δοθιήν, -ένος (nom commun) (m)''' : furoncle.<br>
'''δόκησις, -ήσεως (nom commun) (f)''' : Opinion ; croyance.<br>
'''δοκέω (verbe)''' : Penser, supposer. Sembler.<br>
'''δολερός, -ή, -όν (adjectif)''' : rusé.<br>
'''δολιόω (verbe)''' : abuser.<br>
'''δόλος, -ου (nom commun) (m)''' : ruse.<br>
'''δόξα, -ης (nom commun) (f)''' : Opinion, vue, point de vue, conjecture, supposition. (Dans le Nouveau Testament.) Gloire, honneur ; splendeur.<br>
'''δοξάζω (verbe)''' : Imaginer. Glorifier.<br>
'''δοξολογία, -ας (nom commun) (f)''' : doxologie.<br>
'''δοξόλογος, -όγου (nom commun) (m)''' : doxologue.<br>
'''δοξοσοφία, -ας (nom commun) (f)''' : doxosophie.<br>
'''δοξόσοφος, -όφου (nom commun) (m)''' : doxosophe.<br>
'''δουλεία, -ας (nom commun) (f)''' : servitude.<br>
'''δουλεύω (verbe)''' : être esclave. Travailler à gages, faire un travail mercenaire.<br>
'''δούλη, -ης (nom commun) (f)''' : esclave.<br>
'''δοῦλος, -ύλου (nom commun) (m)''' : esclave.<br>
'''δουλόω (verbe)''' : asservir.<br>
'''δούξ, -κός (nom commun) (m)''' : chef.<br>
'''δόμος, -ου (nom commun) (m)''' : Maison, palais ; chambre, appartement.<br>
'''δομή, -ῆς (nom commun) (f)''' : structure.<br>
'''δόμησις, -ήσεως (nom commun) (f)''' : construction.<br>
'''δομικός, -ή, -όν (suffixe)''' : structurel.<br>
'''δομικῶς (adverbe)''' : structurellement.<br>
'''δομικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''δομικός''.<br>
'''δομικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''δομικός''.<br>
'''δομικώτατα, -, - (adverbe)''' : Superlatif de ''δομικῶς''.<br>
'''δομικώτερον, -, - (adverbe)''' : Comparatif de ''δομικῶς''.<br>
'''δόναξ, -κος (nom commun) (m)''' : Roseau ; objet fait de roseau.<br>
'''δονέω (verbe)''' : agiter.<br>
'''δόνημα, -ήματος (nom commun) (n)''' : agitation.<br>
'''δορκάς, -δος (f)''' : chevreuil.<br>
'''δοράκινον, -ίνου (nom commun) (n)''' : pêche (fruit).<br>
'''δόρκος, -ου (nom commun) (m)''' : Forme de ''δορκάς''.<br>
'''δόρκων, -ος (nom commun) (m)''' : Forme de ''δορκάς''.<br>
'''δόρξ, -κος (nom commun) (m)''' : Forme de ''δορκάς''.<br>
'''δόσις, -εως (nom commun) (f)''' : don ; cadeau.<br>
'''δοχεῖον, -ίου (nom commun) (n)''' : récipient.<br>
'''δράκαινα, -ίνης (nom commun) (f)''' : dracène.<br>
'''δράκων, -οντος (nom commun) (m)''' : dragon.<br>
'''δρᾶμα, -άματος (nom commun) (n)''' : action théâtrale ; pièce de théâtre.<br>
'''δραπέτης, -ου (nom commun) (m)''' : fugitif.<br>
'''δράσσομαι (verbe)''' : saisir.<br>
'''δραχμή, -ῆς (nom commun) (f)''' : Poignée, contenu de la main. Drachme attique.<br>
'''δράω (verbe)''' : .<br>
'''δρίλαξ, -κος (nom commun) (m)''' : sangsue.<br>
'''δρομαῖος, -ία, -ῖον (adjectif)''' : .<br>
'''δρομάς, -δος (nom commun) (m)''' : dromadaire.<br>
'''δρόμος, -ου (nom commun) (m)''' : Course (de chevaux), lutte à la course, tour de promenade.<br>
'''δρόσος, -ου (nom commun) (f)''' : rosée.<br>
'''δρυμός, -οῦ (nom commun) (m)''' : bois (lieu), forêt.<br>
'''δύη, -ης (nom commun) (f)''' : misère.<br>
'''δύναμις, -εως (nom commun) (f)''' : force en puissance.<br>
'''δύο (adjectif numéral)''' : deux.<br>
'''δύσις, -εως (nom commun) (f)''' : ouest.<br>
'''δυσλογία, -ας (nom commun) (f)''' : .<br>
'''δυσμικός, -ή, -όν (adjectif)''' : occidental.<br>
'''δυσ- (préfixe)''' : Difficulté, malheur.<br>
'''δυσσέϐεια, -ίας (nom commun) (f)''' : impiété.<br>
'''δυσσεϐέστατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''δυσσεϐής''.<br>
'''δυσσεϐέστερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''δυσσεϐής''.<br>
'''δυσσεϐής, -ής, -ές (adjectif)''' : impie.<br>
'''δυσσεϐῶς (adverbe) : impieusement.<br>
'''δυστύχημα, -ήματος (nom commun) (n)''' : accident.<br>
'''δυστυχής, -ής, -ές (adjectif)''' : malheureux.<br>
'''δυστυχία, -ας (nom commun) (f)''' : malheur.<br>
'''δυστυχῶς (adverbe)''' : malheureusement.<br>
'''δυστυχῶ (verbe)''' : causer un malheur.<br>
'''δυτικός, -ή, -όν (adjectif)''' : occidental.<br>
'''δυτικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''δυτικός''.<br>
'''δυτικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''δυτικός''.<br>
'''δυτικότατα, -, - (adverbe)''' : Superlatif de ''δυτικῶς''.<br>
'''δυτικότερον, -, - (adverbe)''' : Comparatif de ''δυτικῶς''.<br>
'''δυτικῶς (adverbe)''' : occidentalement.<br>
'''δύω (verbe)''' : S’enfoncer, se plonger. (Par extension) Pénétrer dans. (Par analogie) Se revêtir de.<br>
'''δώδεκα (adjectif numéral)''' : douze.<br>
'''δωδεκάς, -δος (nom commun) (f)''' : douzaine.<br>
'''δῶμα, -ώματος (nom commun) (n)''' : Construction. Maison, demeure. Chambre principale. Temple (demeure d’un dieu).<br>
'''δωμάτιον, -ίου (nom commun) (n)''' : chambre.<br>
'''δωράκινον, -ίνου (nom commun) (n)''' : pêche (fruit).<br>
'''δωρικός, -ή, -όν (adjectif)''' : dorien.<br>
'''δωροδοκῶ (verbe)''' : corrompre, suborner.<br>
'''δῶρον, -ώρου (nom commun) (n)''' : Don ; présent. Paume de la main ; palme.<br>
'''δώτωρ, -ορος (nom commun) (m)''' : donneur.<br>
'''δῶ (verbe)''' : Lier, attacher. (Par extension) Enfermer, emprisonner. (Par analogie) Entraver, empêcher, retenir. (Figuré) Lier, enchaîner. Manquer, avoir besoin de. (Impersonnel) Il est besoin de, il faut. (Forme moyenne) Avoir besoin.
(Par suite) Demander, prier.<br>
'''Δαϐίδ (nom propre) (m)''' : Forme alternative de ''Δαυίδ''.<br>
'''Δαίδαλος, -άλου (nom propre) (m)''' : Dédale.<br>
'''Δάκης, -ου (nom commun) (n)''' : Dace.<br>
'''Δαμασκός, -οῦ (nom propre) (f)''' : Damas.<br>
'''Δαμαστής, -οῦ (nom propre) (m)''' : Damastès (Autre surnom de Polypémon).<br>
'''Δαμάτηρ, -τρός (nom propre) (f)''' : Forme arcado-chypriote, béotienne, et dorienne de ''Δημήτηρ''.<br>
'''Δαμιανός, -οῦ (nom propre) (m)''' : Damien.<br>
'''Δαμία, -ας (nom propre) (f)''' : Damia.<br>
'''Δάμις, - (nom propre) (m)''' : Damis.<br>
'''Δάν, -ός (nom propre) (m)''' : Forme de ''Ζεύς''.<br>
'''Δανιήλ (nom propre) (m)''' : Daniel.<br>
'''Δάνος, -ου (nom commun) (m)''' : Forme macédonienne de ''Θάνατος''.<br>
'''Δαυείδ (nom propre) (m)''' : Forme alternative de ''Δαυίδ''.<br>
'''Δαυίδ (nom propre) (m)''' : David.<br>
'''Δεῖμος, -ίμου (nom propre) (m)''' : Déimos.<br>
'''Δεινώ, -οῦς (nom propre) (f)''' : Dino. (une des Grées)<br>
'''Δέσποινα, -ίνης (nom propre) (f)''' : [[wikt:Despina|Despina]].<br>
'''Δεύς, -ιός (nom propre) (m)''' : Forme laconienne de ''Ζεύς''.<br>
'''Δῃάνειρα, -ίρας (nom propre) (f)''' : Déjanire.<br>
'''Δηϊδάμεια, -ίας (nom propre) (f)''' : Déidamie.<br>
'''Δηιόκης, -ου (nom propre) (m)''' : Déjocès.<br>
'''Δηΐφοϐος, -όϐου (nom propre) (m)''' : Déiphobe.<br>
'''Δημήτηρ, -τρος (nom propre) (f)''' : [[wikt:Déméter|Déméter]].<br>
'''Δημήτριος, -ίου (nom propre) (m)''' : Démétrios.<br>
'''Δημιουργός, -οῦ (nom propre) (m)''' : Créateur.<br>
'''Δημοσθένης, -ους (nom propre) (m)''' : Démosthène.<br>
'''Δημώναξ, -ώνακτος (nom propre) (m)''' : Démonax.<br>
'''Δίδυμοι, -ων (nom propre) (m)''' : Gémeaux.<br>
'''Δίκη, -ης (nom propre) (f)''' : [[wikt:Dicé|Dicé]]. (Déesse de la justice divine.)<br>
'''Δίκτυον, -ύου (nom propre) (n)''' : Réticule.<br>
'''Διόδωρος, -ώρου (nom propre) (m)''' : Diodore.<br>
'''Διογένης, -ους (nom propre) (m)''' : Diogène.<br>
'''Διόνυσος, -ύσου (nom propre) (m)''' : [[wikt:Dionysos|Dionysos]].<br>
'''Διομέδων, -οντος (nom propre) (m)''' : Diomède.<br>
'''Δίς, -ιός (nom propre) (m)''' : Forme rare de ''Ζεύς''.<br>
'''Διώνη, -ης (nom propre) (f)''' : [[wikt:Dioné|Dioné]].<br>
'''Δούναϐις, -άϐεως (nom propre) (m)''' : Danube.<br>
'''Δύμη, -ης (nom propre) (f)''' : Dymé.<br>
'''Δυσνομία, -ας (nom propre) (m)''' : Dysnomie.<br>
'''Δωμάτηρ, -τρός (nom propre) (f)''' : Forme éolienne de ''Δημήτηρ''.<br>
'''Δωριεύς, -έως (nom commun) (m)''' : Dorien.<br>
'''Δωρίς, -δος (nom propre) (f)''' : Doris. (Océanide) (nom commun) Dorienne.<br>
'''Δωροθέα, -ας (nom propre) (f)''' : Dorothée.<br>
'''Δωρόθεος, -έου (nom propre) (m)''' : Dorothéos.<br>
'''Δῶρος, -ώρου (nom propre) (m)''' : Doros.<br>
==Ε==
'''ἐάν (conjonction)''' : si (éventuel).<br>
'''ἔαρ, -ος (nom commun) (n)''' : printemps.<br>
'''ἑϐδομάς, -δος (nom commun) (f)''' : semaine.<br>
'''ἑϐδομήκοντα (adjectif numéral)''' : soixante-dix.<br>
'''ἕϐδομος, -όμη, -ομον (adjectif numéral)''' : septième.<br>
'''ἑϐραΐζω (verbe)''' : .<br>
'''ἑϐραῖος, -ία, -ῖον (adjectif)''' : israélite.<br>
'''ἑϐραϊκός, -ή, -όν (adjectif)''' : hébraïque.<br>
'''ἑϐραϊστί (adverbe)''' : en hébreu.<br>
'''ἐγγόνη, -ης (nom commun) (f)''' : petite-fille.<br>
'''ἐγγύησις, -ήσεως (nom commun) (f)''' : garantie.<br>
'''ἐγγύς (adverbe)''' : proche.<br>
'''ἐγγυῶμαι (verbe)''' : garantir (se rendre garant, répondre d’une chose, du maintien, de l’exécution d’une chose).<br>
'''ἐγείρω (verbe)''' : réveiller.<br>
'''ἐγκέφαλος, -άλου (nom commun) (m)''' : cerveau.<br>
'''ἔγκλησις, -ίσεως (nom commun) (f)''' : accusation.<br>
'''ἔγκλισις, -ίσεως (nom commun) (f)''' : mode (grammaire).<br>
'''ἐγκόπρησις, -ήσεως (nom commun) (f)''' : incontinence fécale.<br>
'''ἐγκόσμιος, -α, -ο (adjectif)''' : .<br>
'''ἔγκυος, -ος, -ον (adjectif)''' : enceinte.<br>
'''ἐγκυμονῶ (verbe)''' : engrossir.<br>
'''ἐγκύμων, -ων, -ον (adjectif)''' : .<br>
'''ἐγχειρίδιον, -ίου (nom commun) (n)''' : manuel ; poignard.<br>
'''ἐγώ (pronom personnel)''' : je.<br>
'''ἔδαφος, -άφους (nom commun) (n)''' : sol.<br>
'''ἐδεμικός, -ή, -όν (adjectif)''' : édénique.<br>
'''ἐδεμικῶς (adverbe)''' : édéniquement.<br>
'''ἐδεμικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐδεμικός''.<br>
'''ἐδεμικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐδεμικός''.<br>
'''ἐδεμικώτατα, -, - (adverbe)''' : Superlatif de ''ἐδεμικῶς''.<br>
'''ἐδεμικώτερον, -, - (adverbe)''' : Comparatif de ''ἐδεμικῶς''.<br>
'''ἔδεσμα, -έσματος (nom commun) (n)''' : nourriture.<br>
'''ἕδρα, -ας (nom commun) (f)''' : Siège. Trône. Résidence, demeure. Partie du corps sur laquelle on s'assied. Action de s'asseoir. Assemblée siégeante.<br>
'''ἑδραῖος, ία, -ῖον (adjectif)''' : .<br>
'''ἐδώδιμος, -η, -ο (adjectif)''' : mangeable.<br>
'''ἐδωδή, -ῆς (nom commun) (f)''' : .<br>
'''ἔδω (verbe)''' : nourrir.<br>
'''ἕζομαι (verbe)''' : asseoir.<br>
'''ἔζω (verbe)''' : nourrir.<br>
'''ἐθίζω (verbe)''' : accoutumer.<br>
'''ἐθικός, -ή, -όν (adjectif)''' : coutumier.<br>
'''ἐθικῶς (adverbe)''' : coutumièrement.<br>
'''ἐθικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐθικός''.<br>
'''ἐθικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐθικός''.<br>
'''ἐθνικός, -ή, -όν (adjectif)''' : national.<br>
'''ἐθνικῶς (adverbe)''' : sagement.<br>
'''ἐθνικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐθνικός''.<br>
'''ἐθνικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐθνικός''.<br>
'''ἔθνος, -ους (nom commun) (n)''' : Famille, ensemble des proches. Nation. Tribu. (Religion) (Au pluriel) Gentils, les non-juifs, usage tardif dans la Bible. Troupeau.<br>
'''ἔθος, -ους (nom commun) (n)''' : coutume.<br>
'''ἔθω (verbe)''' : avoir coutume de ; être habituel.<br>
'''-ειδής, -ής, -ές (suffixe)''' : en forme de.<br>
'''-ειδῶς (suffixe)''' : .<br>
'''-ειδέστατος, -άτη, -έστατον (suffixe)''' : Superlatif de ''-ειδής''.<br>
'''-ειδέστερος, -έρα, -έστερον (suffixe)''' : Comparatif de ''-ειδής''.<br>
'''εἰδοποίησις, -ήσεως (nom commun) (f)''' : information.<br>
'''εἰδοποιῶ (verbe)''' : informer.<br>
'''εἶδος, -ἴδους (nom commun) (f)''' : Forme du corps ; air d'une personne ou d'une chose.<br>
'''εἰδύλλιον, -ίου (nom commun) (n)''' : Petit poème lyrique.<br>
'''εἰδωλολάτρης, -ου (nom commun) (m)''' : païen.<br>
'''εἰδωλολατρία, -ας (nom commun) (f)''' : paganisme.<br>
'''εἴδωλον, -ώλου (nom commun) (n)''' : Simulacre, fantôme ; image, portrait.<br>
'''εἴδω (verbe)''' : voir.<br>
'''εἰ (adverbe, conjonction)''' : si.<br>
'''εἰ μή (conjonction)''' : à moins que ; sauf si.<br>
'''εἰκονικός, -ή, -όν (adjectif)''' : virtuel.<br>
'''εἰκός, -τος (nom commun) (n)''' : raison.<br>
'''εἰκών, -όνος (nom commun) (f)''' : image, portrait.<br>
'''εἴκω (verbe)''' : Être semblable ; ressembler.<br>
'''εἰσαφίημι (verbe)''' : .<br>
'''εἰς (adverbe ; préposition)''' : Dans. Jusqu’à ; vers.<br>
'''εἷς, ἑνός (adjectif numéral)''' : un.<br>
'''εἰκοσαριά, -ᾶς (nom commun) (f)''' : vingtaine.<br>
'''εἴκοσι(ν) (adjectif numéral)''' : vingt.<br>
'''ἐείκοσι(ν) (adjectif numéral)''' : Forme homérique de ''εἴκοσι(ν)''.<br>
'''ϝίκατι(ν) (adjectif numéral)''' : Forme béotienne de ''εἴκοσι(ν)''.<br>
'''ϝείκατι(ν) (adjectif numéral)''' : Forme dorienne de ''εἴκοσι(ν)''.<br>
'''βείκατι(ν) (adjectif numéral)''' : Forme sud-orientale de ''εἴκοσι(ν)''.<br>
'''εἷλιξ, -κος (nom commun) (f)''' : Forme poétique de ''ἕλιξ''.<br>
'''εἰλύω (verbe)''' : .<br>
'''εἴλω (verbe)''' : tourner ; enrouler. Entasser.<br>
'''εἷμα, -ἵματος (nom commun) (n)''' : Couverture ; vêtement.<br>
'''εἶμι (verbe)''' : aller, se déplacer.<br>
'''εἰμί (verbe)''' : être.<br>
'''-εῖον, -ίου (suffixe) (n)''' : lieu caractéristique.<br>
'''εἶπα (verbe)''' : Forme ionienne de ''εἶπον''.<br>
'''εἴπην (verbe)''' : Forme dorienne de ''εἶπον''.<br>
'''ἔειπον (verbe)''' : Forme homérique de ''εἶπον''.<br>
'''εἶπον (verbe)''' : dire, parler.<br>
'''εἰρωνεία, -ας (nom commun) (f)''' : dissimulation.<br>
'''εἴρων, -ος (nom commun) (m/f)''' : dissimulateur.<br>
'''εἰσϐάλλω (verbe)''' : envahir.<br>
'''εἰσϐδάλλω (verbe)''' : .<br>
'''εἰσϐολέυς, -έως (nom commun) (m)''' : envahisseur.<br>
'''εἰσϐολή, -ῆς (nom commun) (f)''' : invasion.<br>
'''εἰσχωρῶ (verbe)''' : pénétrer.<br>
'''ἕκαστος, -άστη, -αστον (adjectif)''' : Chaque, chacun.<br>
'''ἑκάτερος, -έρα, -άτερον (adjectif)''' : L’un de deux, chacun des deux.<br>
'''ἑκατόν (adjectif numéral)''' : cent.<br>
'''ἑκατομμύριον, -ίου (nom commun) (n)''' : million.<br>
'''ἑκατονταρχία, -ας (nom commun) (f)''' : centurie.<br>
'''ἑκατόνταρχος, -άρχου (nom commun) (m)''' : centurion.<br>
'''ἐκϐιάζω (verbe)''' : faire chanter.<br>
'''ἐκϐιασμός, -οῦ (nom commun) (m)''' : chantage.<br>
'''ἐκδίδω (verbe)''' : éditer.<br>
'''ἐκδίκησις, -ήσεως (nom commun) (f)''' : vengeance.<br>
'''ἐκδικητής, -οῦ (nom commun) (m)''' : vengeur.<br>
'''ἐκδικητικός, -ή, -ον (adjectif)''' : vengeur.<br>
'''ἐκδικῶ (verbe)''' : venger.<br>
'''ἔκδοσις, -όσεως (nom commun) (f)''' : édition.<br>
'''ἐκδοχή, -ῆς (nom commun) (f)''' : version.<br>
'''ἐκδύω (verbe)''' : Faire disparaître, ôter.<br>
'''ἐκ (adverbe ; préposition ; préfixe)''' (Devient ''ἐξ'' devant un mot commençant par une voyelle, et ''ἐγ'' devant un mot commençant par ''β'', ''δ'', ''λ'' ou ''μ''.) : Hors, dehors.<br>
'''ἐκεῖθεν (adverbe démonstratif)''' : de là-bas.<br>
'''ἐκεῖ (adverbe démonstratif)''' : là-bas (sans mouvement).<br>
'''ἐκεῖσε (adverbe démonstratif)''' : là-bas (avec mouvement).<br>
'''ἐκείνῃ (adverbe démonstratif)''' : par là-bas.<br>
'''ἔκζεμα, -έματος (nom commun) (n)''' : eczéma.<br>
'''ἑκηϐόλος, -όλου (nom commun) (m)''' : tireur d’élite.<br>
'''ἔκθεσις, -έσεως (nom commun) (f)''' : exposition.<br>
'''ἐκκαλέω (verbe)''' : sommer.<br>
'''ἐκκένωσις, -ώσεως (nom commun) (f)''' : évacuation.<br>
'''ἐκκενῶ (verbe)''' : évacuer.<br>
'''ἐκκεντρικός, -ή, -όν (adjectif)''' : excentrique.<br>
'''ἐκκίνησις, -ήσεως (nom commun) (f)''' : départ.<br>
'''ἐκκλεισία, -ας (nom commun) (f)''' : Forme thessalienne de ''ἐκκλησία''.<br>
'''ἐκκλησία, -ας (nom commun) (f)''' : assemblée.<br>
'''ἐκκλησίασμα, -άσματος (nom commun) (n)''' : congrégation.<br>
'''ἐκλέγω (verbe)''' : choisir, sélectionner.<br>
'''ἐκλεκτικός, -ή, -όν (adjectif)''' : sélectif.<br>
'''ἐκλογή, -ῆς (nom commun) (f)''' : choix, élection.<br>
'''ἐκμιαίνομαι (verbe)''' : éjaculer.<br>
'''ἐκπομπή, -ῆς (nom commun) (f)''' : émission.<br>
'''ἐκπέμπω (verbe)''' : émettre.<br>
'''ἔκστασις, -άσεως (nom commun) (f)''' : transport spirituel.<br>
'''ἐκτάμνω (verbe)''' : Forme homérique et ionienne de ''ἐκτέμνω''.<br>
'''ἐκτέμνω (verbe)''' : .<br>
'''ἐκθέτης, -ου (nom commun) (m)''' : exposant.<br>
'''ἐκθετικός, -ή, -όν (adjectif)''' : exponentiel.<br>
'''ἔκρηξις, -ήξεως (nom commun) (f)''' : explosion.<br>
'''ἐκτίθημι (verbe)''' : exposer.<br>
'''ἑκτικός, -ή, -όν (adjectif)''' : habituel.<br>
'''ἑκτικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἑκτικός''.<br>
'''ἑκτικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἑκτικός''.<br>
'''ἑκτικῶς (adverbe)''' : habituellement.<br>
'''ἐκτιμῶ (verbe)''' : estimer.<br>
'''-εκτομία, -ας (suffixe)''' : action de couper.<br>
'''ἐκτός (adverbe ; préposition)''' : au-dehors ; hors de.<br>
'''ἑκυρά, -ᾶς (nom commun) (f)''' : belle-mère.<br>
'''ἑκυρός, -οῦ (nom commun) (m)''' : beau-père.<br>
'''ἐκφεύγω (verbe)''' : échapper à.<br>
'''ἔκφρασις, -άσεως (nom commun) (f)''' : expression.<br>
'''ἐκφράζω (verbe)''' : exprimer.<br>
'''ἐκφοϐῶ (verbe)''' : intimider.<br>
'''ἐκχύμωμα, -ώµατος (nom commun) (n)''' : Forme alternative de ''ἐκχύμωσις''.<br>
'''ἐκχύμωσις, -ώσεως (nom commun) (f)''' : bleu.<br> (tache de sang extravasé).<br>
'''ἐκχῶ (verbe)''' : s’écouler.<br>
'''ἐλάσσων, -ων, -ον (adjectif)''' : Comparatif de ''ἐλαχύς''.<br>
'''ἐλάττωμα, -ώµατος (nom commun) (n)''' : vice.<br>
'''ἔλαφος, -άφου (nom commun) (m/f)''' : Cerf ; biche.<br>
'''ἐλαφρός, -ή, -όν (adjectif)''' : léger, leste ; agile.<br>
'''ἐλαφρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἐλαφρός''.<br>
'''ἐλαφρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἐλαφρός''.<br>
'''ἐλαφρῶς (adverbe)''' : légèrement, lestement ; agilement.<br>
'''ἐλαχέως (adverbe)''' : petitement, courtement ; moyennement.<br>
'''ἐλάχιστος, -ίστη, -άχιστον (adjectif)''' : Superlatif de ''ἐλαχύς''.<br>
'''ἐλαχύς, -άχεια, -ύ (adjectif)''' : petit, court ; moyen.<br>
'''ἔλεγχος, -έγχους (nom commun) (n)''' : examen ; réfutation.<br>
'''ἐλέγχω (verbe)''' : examiner ; réfuter.<br>
'''ἐλεεινός, -ή, -όν (adjectif)''' : compatissant.<br>
'''ἐλέφας, -αντος (nom commun) (m)''' : Éléphant ; dent d’éléphant ; défense d’éléphant, ivoire.
Objet garni d’ivoire ou ressemblant à de l’ivoire.<br>
'''ἐλεημοσύνη, -ης (nom commun) (f)''' : aumône.<br>
'''ἐλεημονέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἐλεήμων''.<br>
'''ἐλεημονέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''ἐλεήμων''.<br>
'''ἐλεήμονως (adverbe)''' : charitablement.<br>
'''ἐλεήμων, -ων, -ον (adjectif)''' : charitable.<br>
'''ἔλεος, -έου (nom commun) (f)''' : Pitié ; compassion.<br>
'''ἐλευθερία, -ας (nom commun) (f)''' : liberté.<br>
'''ἐλευθερίη, -ας (nom commun) (f)''' : Forme ionienne de ''ἐλευθερία''.<br>
'''ἐλεύθερος, -έρα, ύθερον (adjectif)''' : libre. Qui convient à un homme libre, digne d’un homme libre.<br>
'''ἐλευθέρως (adverbe)''' : librement.<br>
'''ἐλευθερώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐλεύθερος''.<br>
'''ἐλευθερώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐλεύθερος''.<br>
'''ἐλελεῦ (verbe)''' : pousser un cri de guerre.<br>
'''ἐλελελεῦ (nom commun)''' : cri de guerre.<br>
'''ἐλεῶ (verbe)''' : avoir pitié.<br>
'''ἕλιξ, -κος (nom commun) (f)''' : Spirale, vrille. (Par analogie) Oreille externe.<br>
'''ἑλίσσω (verbe)''' : tourner autour.<br>
'''ἕλκηθρον, -ήτρου (nom commun) (n)''' : traîneau.<br>
'''ἕλκω (verbe)''' : hisser.<br>
'''ἐλλείπω (verbe)''' : Laisser derrière soi. Laisser de côté, négliger ; omettre. Manquer, faire défaut. (Intransitif) Rester en arrière.<br>
'''ἔλλειψις, -ίψεως (nom commun) (f)''' : Manque, défaut ; insuffisance.<br>
'''ἑλληνίζω (verbe)''' : parler grec.<br>
'''ἑλληνικός, -ή, -όν (adjectif)''' : grec.<br>
'''ἑλληνικῶς (adverbe)''' : .<br>
'''ἑλληνικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἑλληνικός''.<br>
'''ἑλληνικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἑλληνικός''.<br>
'''ἑλληνιστής, -οῦ (nom commun) (m)''' : homme parlant grec.<br>
'''ἑλληνιστί (adverbe)''' : en grec.<br>
'''ἑλληνίστρια, -ας (nom commun) (f)''' : femme parlant grec.<br>
'''ἐλλός, -οῦ (nom commun) (m)''' : faon.<br>
'''ἐλπίς, -δος (nom commun) (f)''' : espoir.<br>
'''ἔλυτρον, -ύτρου (nom commun) (n)''' : Enveloppe, étui, fourreau. (Par extension) Tout ce qui sert d’enveloppe.<br>
'''ἐλύω (verbe)''' : Entourer, rouler autour.<br>
'''ἔμϐρυον, -ύου (nom commun) (n)''' : embryon.<br>
'''ἔμεσις, -έσεως (nom commun) (f)''' : .<br>
'''ἐμετικός, -ή, -όν (adjectif)''' : vomitif.<br>
'''ἔμετος, -έτου (nom commun) (f)''' : vomissement.<br>
'''ἐµμί (verbe)''' : Forme éolienne de ''εἰμί''.<br>
'''ἐμῶ (verbe)''' : vomir.<br>
'''ἐμμονή, -ῆς (nom commun) (f)''' : obsession.<br>
'''ἐμός, -ή, -όν (adjectif possessif)''' : mon.<br>
'''ἐμπειρία, -ας (nom commun) (f)''' : expérience.<br>
'''ἐμπειρικός, -ή, -όν (adjectif)''' : expérienciel.<br>
'''ἐμπειρικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἐμπειρικός''.<br>
'''ἐμπειρικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἐμπειρικός''.<br>
'''ἐμπειρικότατα, -, - (adverbe)''' : Superlatif de ''ἐμπειρικῶς''.<br>
'''ἐμπειρικότερον, -, - (adverbe)''' : Comparatif de ''ἐμπειρικῶς''.<br>
'''ἐμπειρικῶς (adverbe)''' : expérienciellement.<br>
'''ἐμπλάσσω (verbe)''' : emplâtrer.<br>
'''ἔμπλαστρον, -άστρου (nom commun) (n)''' : emplâtre.<br>
'''ἔμπνευσις, -ύσεως (nom commun) (f)''' : .<br>
'''ἐμπορεῖον, -ίου (nom commun) (n)''' : boutique.<br>
'''ἐμπόρευμα, -ύματος (nom commun) (n)''' : marchandise.<br>
'''ἐμπορεύομαι (verbe)''' : commercer.<br>
'''ἐμπόριον, -ίου (nom commun) (n)''' : commerce.<br>
'''ἔμπορος, -όρου (nom commun) (m)''' : marchand.<br>
'''ἐμπρός (verbe)''' : devant.<br>
'''ἐμφαίνω (verbe)''' : Montrer ; présenter.<br>
'''ἐμφανής, -ής, -ές (adjectif)''' : apparent.<br>
'''ἐμφανίζω (verbe)''' : apparaître.<br>
'''ἐμφάνισις, -ίσεως (nom commun) (f)''' : apparence.<br>
'''ἔμφασις, -άσεως (nom commun) (f)''' : apparence.<br>
'''ἔμφραγμα, -άγματος (nom commun) (n)''' : infarctus.<br>
'''ἐμφρουρῶ (verbe)''' : .<br>
'''ἐμφύσημα, -ήματος (nom commun) (n)''' : inflation.<br>
'''ἐμφυσηματώδης, -ης, -ες (adjectif)''' : inflationnaire.<br>
'''ἐναλλακτικός, -ή, -όν (adjectif)''' : alternatif.<br>
'''ἐναλλακτικῶς (adverbe)''' : alternativement.<br>
'''ἐναλλακτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐναλλακτικός''.<br>
'''ἐναλλακτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐναλλακτικός''.<br>
'''ἐνάρετος, -ος, -ον (adjectif)''' : vertueux.<br>
'''ἔνδεια, -ας (nom commun) (f)''' : indigence.<br>
'''ἐνδεής, -ής, -ής (adjectif)''' : indigent.<br>
'''ἐνδόμυχος, -ος, -ον (adjectif)''' : intime.<br>
'''ἔνδον (adverbe ; préposition)''' : en dedans, intérieurement, à l'intérieur ; au-dedans de, à l'intérieur de.<br>
'''ἔνδος (adverbe ; préposition)''' : Forme dorienne de ''ἔνδον''.<br>
'''ἐνδοτάτω (adverbe)''' : Superlatif de ''ἔνδον''.<br>
'''ἐνδοτέρω (adverbe)''' : Comparatif de ''ἔνδον''.<br>
'''ἔνδυμα, -ύματος (nom commun) (n)''' : vêtement.<br>
'''ἐνέργεια, -ίας (nom commun) (f)''' : force en action.<br>
'''ἐνεργής, -ής, -ής (adjectif)''' : productif.<br>
'''ἐνεργός, -ός, -όν (adjectif)''' : actif.<br>
'''ἐνεργούμενος, -ένου (nom commun) (m)''' : énergumène.<br>
'''ἐνεργῶ (verbe)''' : Agir, produire, accomplir, exécuter. Agir sur, influencer (particulièrement en mauvaise part en parlant du mauvais esprit).
(Voie moyenne) Opérer, agir.<br>
'''ἐνενήκοντα (adjectif numéral)''' : quatre-vingt-dix.<br>
'''ἐνέχομαι (verbe)''' : .<br>
'''ἔνθα (adverbe)''' : ici.<br>
'''ἐνθάδε (adverbe)''' : ici-même.<br>
'''ἐνθένδε (adverbe)''' : d’ici.<br>
'''ἐννέα (adjectif numéral)''' : neuf.<br>
'''ἔνεμα, -έματος (nom commun) (n)''' : lavement. (Remède liquide.)<br>
'''ἔνεσις, -έσεως (nom commun) (f)''' : injection.<br>
'''ἐνεστώς, -ῶτος (nom commun) (n)''' : présent.<br>
'''ἐννῆ (adjectif numéral)''' : Forme de ''ἐννέα''.<br>
'''ἐνέχομαι (verbe)''' : .<br>
'''ἐνίημι (verbe)''' : injecter.<br>
'''ἐν (adverbe ; préposition)''' : Dans, en, parmi.<br>
'''ἐν- (préfixe)''' (Devient ''ἐγ-'' devant ''γ'', ''κ'', ''ξ'', ''χ'' ; ''ἐλ-'' devant ''λ'' ; ''ἐμ-'' devant ''β'', ''µ'', ''π'', ''φ'', ''ψ'' ; ''ἐρ-'' devant ''ρ'' dans quelques mots comme ''ἔρρινον'' ; ''ἐσ-'' devant ''σ''.) : in-.<br>
'''ἐνηλικίωσις, -ώσεως (nom commun) (f)''' : majorité.<br>
'''ἐνήλικος -η -ον (adjectif)''' : adulte.<br>
'''ἐνῆλιξ, -ήλικος (nom commun) (m)''' : adulte.<br>
'''ἔνθεσις, -έσεως (nom commun) (f)''' : enthèse.<br>
'''ἐνθουσιασμός, -οῦ (nom commun) (m)''' : enthousiasme.<br>
'''ἐνθουσιαστικός, -ή, -όν (adjectif)''' : enthousiasmant.<br>
'''ἐνθουσιαστικῶς (adverbe)''' : -ment.<br>
'''ἐνθουσιαστικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐνθουσιαστικός''.<br>
'''ἐνθουσιαστικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐνθουσιαστικός''.<br>
'''ἐνθουσιαστικώτατα, -, - (adverbe)''' : Superlatif de ''ἐνθουσιαστικῶς''.<br>
'''ἐνθουσιαστικώτερον, -, - (adverbe)''' : Comparatif de ''ἐνθουσιαστικῶς''.<br>
'''ἐνθουσιώδης, -ης, -ες (adjectif)''' : enthousiaste.<br>
''', -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐνθουσιώδης''.<br>
''', -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐνθουσιώδης''.<br>
''', -, - (adverbe)''' : Superlatif de ''''.<br>
''', -, - (adverbe)''' : Comparatif de ''''.<br>
'''ἐνίοτε (adverbe)''' : quelquefois.<br>
'''ἔννοια, -ίας (nom commun) (f)''' : concept.<br>
'''ἐνοριακός, -ή, -όν (adjectif)''' : paroissial.<br>
'''ἐνορία, -ας (nom commun) (f)''' : paroisse.<br>
'''ἐνοχή, -ῆς (nom commun) (f)''' : culpabilité.<br>
'''ἕννυμι (verbe)''' : enfiler. (mettre un vêtement)<br>
'''ἑνότης, -τος (nom commun) (f)''' : unité.<br>
'''ἐνούρησις, -ήσεως (nom commun) (f)''' : incontinence urinaire.<br>
'''ἐνουρῶ (verbe)''' : ne pas pouvoir retenir son urine.<br>
'''ἐνοχή, -ῆς (nom commun) (f)''' : culpabilité.<br>
'''ἐνόχλησις, -ήσεως (nom commun) (f)''' : Dérangement ; embêtement.<br>
'''ἐνοχλητικός, -ή, -όν (adjectif)''' : Dérangeant ; embêtant.<br>
'''ἐνοχλῶ (verbe)''' : Déranger ; embêter.<br>
'''ἐνσκήπτω (verbe)''' : sévir.<br>
'''ἔνταξις, -άξεως (f)''' : insertion.<br>
'''ἐντάσσω (verbe)''' : insérer.<br>
'''ἐνταῦθα (adverbe)''' : là.<br>
'''ἐντίθημι (verbe)''' : introduire.<br>
'''ἐντρέπω (verbe)''' : .<br>
'''ἐντύπωσις, -ώσεως (nom commun) (f)''' : impression.<br>
'''ἐντυπῶ (verbe)''' : .<br>
'''ἕνωσις, -ώσεως (nom commun) (f)''' : union.<br>
'''ἐνώτιον, -ίου (nom commun) (n)''' : boucle d'oreille.<br>
'''ἔορ, -ος (nom commun) (f)''' : Fille ; cousine.<br>
'''ἐρείκη, -ης (nom commun) (f)''' : bruyère.<br>
'''ἑορτάζω (verbe)''' : fêter.<br>
'''ἑορτή, -ῆς (nom commun) (f)''' : fête.<br>
'''ἐξαδελφή, -ῆς (nom commun) (f)''' : cousine.<br>
'''ἐξάδελφος, -ου (nom commun) (m)''' : cousin.<br>
'''ἐξαγοράζω (verbe)''' : soudoyer.<br>
'''ἐξαίρεσις, -έσεως (nom commun) (m)''' : extraction.<br>
'''ἐξαίρω (verbe)''' : exalter.<br>
'''ἐξαιρῶ (verbe)''' : retirer.<br>
'''ἐξαίσιος, -ια, -ιον (adjectif)''' : .<br>
'''ἑξαν (adverbe)''' : Forme dorienne de ''ἑξῆς''.<br>
'''ἔξαρσις, -άρσεως (nom commun) (f)''' : exaltation.<br>
'''ἐξάρτημα, -ήματος (nom commun) (n)''' : accessoire.<br>
'''ἕξ (adjectif numéral) (m/f/n)''' : six.<br>
'''ϝέξ (adjectif numéral) (m/f/n)''' : Forme dorienne de ''ἕξ''.<br>
'''ἑξακόσιοι (adjectif numéral)''' : six-cents.<br>
'''ἐξάντλησις, -ήσεως (nom commun) (f)''' : épuisement.<br>
'''ἐξαντλῶ (verbe)''' : épuiser.<br>
'''ἐξαπάτησις, -ήσεως (nom commun) (f)''' : supercherie.<br>
'''ἐξαπατῶ (verbe)''' : .<br>
'''ἐξέδρα, -ας (nom commun) (f)''' : plate-forme.<br>
'''ἐξέγερσις, -έρσεως (nom commun) (f)''' : insurrection.<br>
'''ἑξείης (adverbe)''' : Forme poétique de ''ἑξῆς''.<br>
'''ἐξήγησις, -ήσεως (nom commun) (f)''' : Exposé. Explication.<br>
'''ἐξηγητής, -ου (nom commun) (m)''' : .<br>
'''ἐξἡγοῦμαι (verbe)''' : Conduire, exposer.<br>
'''ἑξήκοντα (adjectif numéral)''' : soixante.<br>
'''ἑξῆς (adverbe)''' : .<br>
'''ἐξιλέωσις, -ώσεως (nom commun) (f)''' : expiation.<br>
'''ἐξιλεῶ (verbe)''' : expier.<br>
'''ἐξέλιξις, -ίξεως (nom commun) (f)''' : évolution.<br>
'''ἐξελίσσω (verbe)''' : évoluer.<br>
'''ἐξευτελίζω (verbe)''' : humilier.<br>
'''ἐξέτασις, -άσεως (nom commun) (f)''' : examen, recherche.<br>
'''ἐξίστημι (verbe)''' : déplacer.<br>
'''ἕξις, -εως (nom commun) (f)''' : habitude.<br>
'''ἐξόγκωμα, -ώματος (nom commun) (n)''' : Proéminence ; protubérance.<br>
'''ἔξοδος, -ου (nom commun) (f)''' : Issue ; sortie, départ.<br>
'''ἐξομολόγησις, -ήσεως (nom commun) (f)''' : confession.<br>
'''ἐξομολογητήριον, -ίου (nom commun) (n)''' : confessionnal.<br>
'''ἐξομολογητής, -οῦ (nom commun) (m)''' : confesseur.<br>
'''ἐξομολογῶ (verbe)''' : confesser.<br>
'''ἐξορία, -ας (nom commun) (f)''' : exil.<br>
'''ἐξορίζω (verbe)''' : exorciser.<br>
'''ἐξορκισμός, -οῦ (nom commun) (m)''' : exorcisme.<br>
'''ἐξορκιστής, -οῦ (nom commun) (m)''' : exorciste.<br>
'''ἐξύπνημα, -ήματος (nom commun) (n)''' : réveil.<br>
'''ἔξυπνος, -ος, -ον (adjectif)''' : .<br>
'''ἐξυπνῶ (verbe)''' : réveiller.<br>
'''ἐξωθῶ (verbe)''' : .<br>
'''ἐξωκκλήσιον, -ίου (nom commun) (n)''' : chapelle.<br>
'''ἐξωµίς, -δος (nom commun) (f)''' : exomide.<br>
'''ἐξώστης, -ου (nom commun) (m)''' : balcon.<br>
'''ἐξωτερικός, -ή, -όν (adjectif)''' : extérieur, externe.<br>
'''ἐξωτερικῶς (adverbe)''' : extérieurement.<br>
'''ἐξωτερικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐξωτερικός''.<br>
'''ἐξωτερικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐξωτερικός''.<br>
'''ἐξωτερικώτατα, -, - (adverbe)''' : Superlatif de ''ἐξωτερικῶς''.<br>
'''ἐξωτερικώτερον, -, - (adverbe)''' : Comparatif de ''ἐξωτερικῶς''.<br>
'''ἐξωτικός, -ή, -όν (adjectif)''' : étranger.<br>
'''ἐξωτικῶς (adverbe)''' : exotiquement.<br>
'''ἐξωτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐξωτικός''.<br>
'''ἐξωτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐξωτικός''.<br>
'''ἐξωτικώτατα, -, - (adverbe)''' : Superlatif de ''ἐξωτικῶς''.<br>
'''ἐξωτικώτερον, -, - (adverbe)''' : Comparatif de ''ἐξωτικῶς''.<br>
'''ἔξω (adverbe)''' : hors de.<br>
'''ἐπαινῶ (verbe)''' : vanter.<br>
'''ἐπαναλαμϐάνω (verbe)''' : répéter ; reprendre.<br>
'''ἐπαλήθευσις, -ύσεως (nom commun) (f)''' : vérification.<br>
'''επαληθευτικός, -ή, -όν (adjectif)''' : vérificatif.<br>
'''ἐπαληθευτικῶς (adverbe)''' : vérificativement.<br>
'''ἐπαληθευτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''επαληθευτικός''.<br>
'''ἐπαληθευτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''επαληθευτικός''.<br>
'''ἐπαληθευτικώτατα, -, - (adverbe)''' : Superlatif de ''επαληθευτικῶς''.<br>
'''ἐπαληθευτικώτερον, -, - (adverbe)''' : Comparatif de ''επαληθευτικῶς''.<br>
'''ἐπαληθεύω (verbe)''' : vérifier.<br>
'''ἐπαναληπτικός, -ή, -όν (adjectif)''' : répétitif.<br>
'''ἐπαναληπτικῶς (adverbe)''' : répétitivement.<br>
'''ἐπαναληπτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐπαναληπτικός''.<br>
'''ἐπαναληπτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐπαναληπτικός''.<br>
'''ἐπαναληπτικώτατα, -, - (adverbe)''' : Superlatif de ''ἐπαναληπτικῶς''.<br>
'''ἐπαναληπτικώτερον, -, - (adverbe)''' : Comparatif de ''ἐπαναληπτικῶς''.<br>
'''ἐπανάληψις, -ήψεως (nom commun) (f)''' : répétition.<br>
'''ἐπανάστασις, -άσεως (nom commun) (f)''' : révolution.<br>
'''ἐπανεκκίνησις, -ήσεως (nom commun) (f)''' : redémarrage.<br>
'''ἐπάνω (adverbe)''' : .<br>
'''ἐπαρχία, -ας (nom commun) (f)''' : province.<br>
'''ἔπαρχος, -άρχου (nom commun) (m)''' : .<br>
'''ἐπάρχω (verbe)''' : .<br>
'''ἐπαφή, -ῆς (nom commun) (f)''' : contact.<br>
'''ἐπαφίημι (verbe)''' : contacter.<br>
'''ἐπεξῆς (adverbe)''' : Forme ionienne de ''ἐφεξῆς''.<br>
'''ἐπένδυσις, -ύσεως (nom commun) (f)''' : .<br>
'''ἐπενδύω (verbe)''' : revêtir par-dessus.<br>
'''ἐπένθεσις, -έσεως (nom commun) (f)''' (Grammaire) Insertion d’une lettre à l’intérieur d’un mot.<br>
'''ἐπεντίθημι (verbe)''' : Insérer une lettre à l’intérieur d’un mot.<br>
'''ἐπερώτησις, -ήσεως (nom commun) (f)''' : interpellation.<br>
'''ἑπτά (adjectif numéral)''' : sept.<br>
'''ἐπεισοδιακός, -ή, -όν (adjectif)''' : épisodique.<br>
'''ἐπεισόδιον, -ου (nom commun) (n)''' : épisode.<br>
'''ἐπιϐαίνω (verbe)''' : Monter. (Marine) Embarquer, monter dans un bateau. (Militaire) Attaquer, avancer sur l’ennemi. Monter à cheval, aller à cheval.<br>
'''ἐπιϐδάλλω (verbe)''' : .<br>
'''ἐπιϐήτωρ, -ορος (nom commun) (m)''' : étalon.<br>
'''ἐπιϐλαϐής, -ής, -ές (adjectif)''' : .<br>
'''ἐπίϐλημα, -ήματος (nom commun) (m)''' : châle.<br>
'''ἐπίγραθμα, -άθματος (nom commun) (n)''' : Forme dorienne de ''ἐπίγραμμα''.<br>
'''ἐπίγραμμα, -άμματος (nom commun) (n)''' : inscription.<br>
'''ἐπίδειξις, -ίξεως (nom commun) (f)''' : exhibition.<br>
'''ἐπιδεκτικός, -ή, -όν (adjectif)''' : .<br>
'''ἐπιδέχομαι (verbe)''' : .<br>
'''ἐπιείκεια, -ίας (nom commun) (f)''' : indulgence.<br>
'''ἐπιεικής, -ής, -ές (adjectif)''' : indulgent.<br>
'''ἐπί (adverbe)''' (Devient ''ἐπ<nowiki>'</nowiki>'' devant une voyelle avec esprit doux et ''ἐφ<nowiki>'</nowiki>'' devant une voyelle avec esprit rude.) : sur.<br>
'''ἐπιθαλάμιον, -ίου (nom commun) (n)''' : .<br>
'''ἐπιθιγγάνω (verbe)''' : .<br>
'''ἐπιτιθέναι (verbe)''' : .<br>
'''ἐπίθεσις, -έσεως (nom commun) (f)''' : attaque ; assaut.<br>
'''ἐπίθετον, -έτου (nom commun) (n)''' : adjectif.<br>
'''ἐπίθετος, -, - (adjectif)''' : adjectival.<br>
'''ἐπίθημα, -ήματος (nom commun) (n)''' : .<br>
'''ἐπιθυμέω (verbe)''' : désirer.<br>
'''ἐπιθυμία, -ας (nom commun) (f)''' : Désir, souhait. Passion.<br>
'''ἐπίθυμος, -, - (adjectif)''' : Désireux ; passionné.<br>
'''ἐπικαλοῦμαι (verbe)''' : invoquer.<br>
'''ἐπικαλύπτω (verbe)''' : recouvrir.<br>
'''ἐπικάλυψις, -ύψεως (nom commun) (f)''' : recouvrement.<br>
'''ἐπικός, -ή, -όν (adjectif)''' : relatif aux vers.<br>
'''ἐπικότατα, -, - (adverbe)''' : Superlatif de ''ἐπικῶς''.<br>
'''ἐπικότερον, -, - (adverbe)''' : Comparatif de ''ἐπικῶς''.<br>
'''ἐπικύρωσις, -ώσεως (nom commun) (f)''' : ratification.<br>
'''ἐπικυρῶ (verbe)''' : ratifier.<br>
'''ἐπικῶς (adverbe)''' : épiquement.<br>
'''ἐπικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐπικός''.<br>
'''ἐπικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐπικός''.<br>
'''ἐπιλαμϐάνω (verbe)''' : invoquer.<br>
'''ἐπίληπτος, -ος, -ον (adjectif)''' : .<br>
'''ἐπιληψία, -ας (nom commun) (f)''' : .<br>
'''ἐπιλογή, -ῆς (nom commun) (f)''' : sélection.<br>
'''ἐπίλογος, -όγου (nom commun) (m)''' : postface.<br>
'''ἐπίπληξις, -ήξεως (nom commun) (f)''' : réprimande.<br>
'''ἐπιπλήττω (verbe)''' : réprimander.<br>
'''ἐπιτίθημι (verbe)''' : attaquer.<br>
'''ἐπιείκεια, -ίας (nom commun) (f)''' : indulgence.<br>
'''ἐπιεικής, -ής -ές (adjectif)''' : indulgent.<br>
'''ἐπιμέλεια, -ίας (nom commun) (f)''' : sollicitude.<br>
'''ἐπιμελής, -ής -ές (adjectif)''' : soigneux.<br>
'''ἐπιμέλησις, -ήσεως (nom commun) (f)''' : soin.<br>
'''ἐπιμελότατα, -, - (adverbe)''' : Superlatif de ''ἐπιμελῶς''.<br>
'''ἐπιμελότερον, -, - (adverbe)''' : Comparatif de ''ἐπιμελῶς''.<br>
'''επιμελοῦμαι (verbe)''' : prendre soin de, veiller à ; se préoccuper de.<br>
'''ἐπιμελῶς (adverbe)''' : soigneusement.<br>
'''ἐπιούσιος, -α, -ον (adjectif)''' : supersubstantiel.<br>
'''ἔπιπλον, -ίπλου (nom commun) (n)''' : fourniture.<br>
'''ἐπίρρημα, -ήματος (nom commun) (n)''' : adverbe.<br>
'''ἐπιρρίπτω (verbe)''' : .<br>
'''ἐπιρρωνύω (verbe)''' : corroborer.<br>
'''ἐπίρρωσις, -ώσεως (nom commun) (f)''' : corroboration.<br>
'''ἐπίσημος, -α, -ον''' : Marqué, noté. Notable, remarquable.<br>
'''ἐπισκέπτης, -ου (nom commun) (m)''' : visiteur.<br>
'''ἐπισκέπτομαι (verbe)''' : visiter.<br>
'''ἐπίσκεψις, -έψεως (nom commun) (f)''' : visite (action de visiter).<br>
'''ἐπιστάζω (verbe)''' : saigner du nez.<br>
'''ἐπίσταμαι (verbe)''' : Savoir ; connaitre.<br>
'''ἐπίσταξις, -άξεως (nom commun) (f)''' : saignement nasal.<br>
'''ἐπιστέλλω (verbe)''' : envoyer.<br>
'''ἐπιστήμη, -ης (nom commun) (f)''' : Science ; savoir.<br>
'''ἐπιστημονέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἐπιστήμων''.<br>
'''ἐπιστημονέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''ἐπιστήμων''.<br>
'''ἐπιστήμονως (adverbe)''' : savamment.<br>
'''ἐπιστήμων, -ων, -ον (adjectif)''' : Savant ; instruit.<br>
'''ἐπιστολή, -ῆς (nom commun) (f)''' : Ordre ou avis émis par un message verbal ou écrit. Message écrit, lettre.<br>
'''ἐπιστολικός, -ή, -όν (adjectif)''' : épistolaire.<br>
'''ἐπιστολικῶς (adverbe)''' : épistolairement.<br>
'''ἐπιστολικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐπιστολικός''.<br>
'''ἐπιστολικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐπιστολικός''.<br>
'''ἐπιστολικώτατα, -, - (adverbe)''' : Superlatif de ''ἐπιστολικῶς''.<br>
'''ἐπιστολικώτερον, -, - (adverbe)''' : Comparatif de ''ἐπιστολικῶς''.<br>
'''ἐπιστροφή, -ῆς (nom commun) (f)''' : retour.<br>
'''ἐπιτακτικός, -ή, -όν (adjectif)''' : impératif.<br>
'''ἐπιτακτικῶς (adverbe)''' : impérativement.<br>
'''ἐπιτακτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐπιτακτικός''.<br>
'''ἐπιτακτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐπιτακτικός''.<br>
'''ἐπιτακτικώτατα, -, - (adverbe)''' : Superlatif de ''ἐπιτακτικῶς''.<br>
'''ἐπιτακτικώτερον, -, - (adverbe)''' : Comparatif de ''ἐπιτακτικῶς''.<br>
'''ἐπιτέμνω (verbe)''' : .<br>
'''ἐπιτήδειος, -α, -ον (adjectif)''' : commode ; pratique.<br>
'''ἐπιτηδειότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἐπιτήδειος''.<br>
'''ἐπιτηδειότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἐπιτήδειος''.<br>
'''ἐπιτηδειότατα, -, - (adverbe)''' : Superlatif de ''ἐπιτηδείως''.<br>
'''ἐπιτηδειότερον, -, - (adverbe)''' : Comparatif de ''ἐπιτηδείως''.<br>
'''ἐπιτηδείως (adverbe)''' : commodément.<br>
'''ἐπιτομή, -ῆς (nom commun) (f)''' : compendium.<br>
'''ἐπιτόνιον, -ίου (nom commun) (n)''' : .<br>
'''ἐπιτρέπω (verbe)''' : autoriser, permettre.<br>
'''ἐπιφέρω (verbe)''' : .<br>
'''ἐπιφώνημα, -ήματος (nom commun) (n)''' : interjection.<br>
'''ἐπιφώνησις, -ήσεως (nom commun) (f)''' : .<br>
'''ἐπιφωνῶ (verbe)''' : .<br>
'''ἐπιχαιρεκακία, -ας (nom commun) (f)''' : .<br>
'''ἐπιχαιρέκακος, -ος, -ον (adjectif)''' : .<br>
'''ἐπιχαίρω (verbe)''' : réjouir.<br>
'''ἐπιχείρημα, -ήματος (nom commun) (n)''' : argument.<br>
'''ἐπιχειρῶ (verbe)''' : cicatriser.<br>
'''ἕπομαι (verbe)''' : argumenter.<br>
'''ἔπος, -ους (nom commun) (n)''' : Parole ; vers.<br>
'''ἐποποιία, -ας (nom commun) (f)''' : épopée.<br>
'''ἐπουλῶ (verbe)''' : cicatriser.<br>
'''ἐπύλλιον, -ίου (nom commun) (n)''' : .<br>
'''ἐπῳδή, -ῆς (nom commun) (f)''' : incantation.<br>
'''ἐπώδυνος, -ος, -ον (adjectif)''' : douloureux. Causé par la douleur.<br>
'''ἑπωµίς, -δος (nom commun) (f)''' : épaulette.<br>
'''ἐπωνύμιον, -ίου (nom commun) (n)''' : nom de famille.<br>
'''ἐπωφελής, -ής, -ές (adjectif)''' : .<br>
'''ἔραμαι (verbe)''' : aimer d'amour, désirer.<br>
'''ἔρανος, -άνου (nom commun) (m)''' : quête.<br>
'''ἐραστής, -οῦ (nom commun) (m)''' : amant.<br>
'''ἑραστός, -ή, -όν (adjectif)''' : aimable, amoureux.<br>
'''ἐράστρια, -ας (nom commun) (f)''' : amante.<br>
'''ἐραστριάω (verbe)''' : être amoureux.<br>
'''ἐρέϐινθος, -ίνθου (nom commun) (m)''' : pois chiche.<br>
'''ἔρεϐος, -έϐους (nom commun) (n)''' : obscurité.<br>
'''ἐρρωσθαι (verbe)''' : se bien porter.<br>
'''ἐργάζομαι (verbe)''' : travailler.<br>
'''ἐργαλεῖον, -ίου (nom commun) (n)''' : outil.<br>
'''ἐργαστήριακός, -ή -όν (adjectif)''' : laborantin.<br>
'''ἐργαστήριον, -ίου (nom commun) (n)''' : atelier, laboratoire.<br>
'''ἐργαστήρ, -ῆρος (nom commun) (m)''' : travailleur.<br>
'''ἐργάτης, -ου (nom commun) (m)''' : ouvrier, travailleur.<br>
'''ἔργον, -ου (nom commun) (n)''' : travail.<br>
'''ἐργώδης, -ης, -ες (adjectif)''' : laborieux.<br>
'''ϝέργον, -ου (nom commun) (n)''' : Forme dorienne de ''ἔργον''.<br>
'''ϝάργον, -ου (nom commun) (n)''' : Forme éléenne de ''ἔργον''.<br>
'''ἐρέσσω (verbe)''' : ramer.<br>
'''ἐρετμόν, -οῦ (nom commun) (n)''' : rame.<br>
'''ἐρεύγομαι (verbe)''' : roter.<br>
'''ἔρευνα, -ας (nom commun) (f)''' : enquête, recherche.<br>
'''ἐρευνητής, -οῦ (nom commun) (m)''' : enquêteur, chercheur.<br>
'''ἐρευνῶ (verbe)''' : enquêter, rechercher.<br>
'''ἐρέφω (verbe)''' : toiturer, couronner.<br>
'''ἐρημίτης, -ου (nom commun) (m)''' : ermite.<br>
'''ἔρημος, -ήμου (nom commun) (f)''' : désert.<br>
'''ἐριθακός, -οῦ (nom commun) (m)''' : rouge-gorge.<br>
'''ἐρίθακος, -άκου (nom commun) (m)''' : rouge-gorge.<br>
'''ἑρμηνεύς, -έως (nom commun) (m)''' : Interprète, traducteur ; messager des dieux.<br>
'''ἑρμηνευτικός, -ή, -όν (adjectif)''' : .<br>
'''ἑρμηνεύω (verbe)''' : Exprimer sa pensée par la parole. (Par suite) Faire connaître, indiquer, exposer (quelque chose). Interpréter, traduire.<br>
'''ἔρομαι (verbe)''' : Demander, enquêter ; s’enquérir de.<br>
'''ἑρπετόν, -οῦ (nom commun) (n)''' : Reptile ; serpent.<br>
'''ἕρπυλλος, -ύλλου (nom commun) (m)''' : serpolet.<br>
'''ἑρπυσμός, -οῦ (nom commun) (m)''' : fluage.<br>
'''ἐρῥωσθαι (verbe)''' : se bien porter.<br>
'''ἔρσην, -ενος (nom commun) (m)''' : Forme crétoise et éolienne de ''ἄρσην''.<br>
'''ἐρεύθω (verbe)''' : rougir.<br>
'''ἔρχομαι (verbe)''' : Venir, aller. S'en aller.<br>
'''ἐρύθημα, -ήματος (nom commun) (n)''' : érythème.<br>
'''ἐρυθρός, -ά, -όν (adjectif)''' : rouge.<br>
'''ἐρυσίπελας, -έλατος (nom commun) (n)''' : inflammation cutanée.<br>
'''ἐρωή, -ῆς (nom commun) (f)''' : précipitation.<br>
'''ἐρωδιός, -οῦ (nom commun) (m)''' : héron.<br>
'''ἐρωμένη, -ης (nom commun) (f)''' : femme aimé.<br>
'''ἐρώμενος, -ένου (nom commun) (m)''' : homme aimé.<br>
'''ἔρως, -τος (nom commun) (n)''' : amour « naturel » ; désir sexuel, plaisir corporel.<br>
'''ἐρώτημα, -ήματος (nom commun) (n)''' : question.<br>
'''ἐρωτηματικός, -ή, -όν (adjectif)''' : interrogatif.<br>
'''ἐρωτηματικότατα, -, - (adverbe)''' : Superlatif de ''ἐρωτηματικῶς''.<br>
'''ἐρωτηματικότερον, -, - (adverbe)''' : Comparatif de ''ἐρωτηματικῶς''.<br>
'''ἐρωτηματικῶς (adverbe)''' : érotiquement.<br>
'''ἐρωτηματικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐρωτηματικός''.<br>
'''ἐρωτηματικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐρωτηματικός''.<br>
'''ἐρώτησις, -ήσεως (nom commun) (f)''' : interrogation.<br>
'''ἐρωτικός, -ή, -όν (adjectif)''' : relatif à l'amour « naturel ».<br>
'''ἐρωτικότατα, -, - (adverbe)''' : Superlatif de ''ἐρωτικῶς''.<br>
'''ἐρωτικότερον, -, - (adverbe)''' : Comparatif de ''ἐρωτικῶς''.<br>
'''ἐρωτικῶς (adverbe)''' : érotiquement.<br>
'''ἐρωτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐρωτικός''.<br>
'''ἐρωτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐρωτικός''.<br>
'''ἐρωτῶ (verbe)''' : interroger.<br>
'''ἐρῶ (verbe)''' : aimer d'amour, désirer. Verser hors de ; vomir.<br>
'''ἐσσαῖος, -ίου (nom commun) (m)''' : essénien.<br>
'''ϝεσή, -ῆς (nom commun) (f)''' : toilettes.<br>
'''ἐσθής, -ήτος (nom commun) (f)''' : vêtement.<br>
'''ἐσθίω (verbe)''' : manger.<br>
'''ἐσθλός, -ή, -όν (adjectif)''' : Excellent ; (poétique) bon.<br>
'''ἐσθλῶς (adverbe)''' : excellemment.<br>
'''ἐσθλώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐσθλός''.<br>
'''ἐσθλώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἐσθλός''.<br>
'''ἐσλός, -ή, -όν (adjectif)''' : Forme dorienne de ''ἐσθλός''.<br>
'''ἑσπέρα, -ας (nom commun) (f)''' : Soir ; ouest.<br>
'''ϝεσπέρα, -ας (nom commun) (f)''' : Forme ancienne de ''ἑσπέρα''.<br>
'''ἕσπερος, -ος, -ον (adjectif)''' : Relatif au soir ; occidental.<br>
'''ἑστίασις, -άσεως (nom commun) (f)''' : restauration.<br>
'''ἑστιατόριον, -ίου (nom commun) (n)''' : restaurant.<br>
'''ἑστιάτωρ, -ορος (nom commun) (m)''' : restaurateur.<br>
'''ἑστιάω (verbe)''' recevoir chez soi.<br>
'''ἐσχάρα, -ας (nom commun) (f)''' : Foyer ; autel domestique et sanctuaire pour les suppliants. Autel pour les sacrifices. Brasier. Réchaud.<br>
'''ἐσχάριον, -ίου (nom commun) (n)''' : cale (dispositif maritime).<br>
'''ἔσχατος, -άτη, -ον (adjectif)''' : dernier.<br>
'''ἐσωτερικός, -ή, -όν (adjectif)''' : intérieur, interne.<br>
'''ἔσω (adverbe)''' : .<br>
'''ἐσωτάτω (adverbe)''' : Superlatif de ''ἔσω''.<br>
'''ἐσωτέρω (adverbe)''' : Comparatif de ''ἔσω''.<br>
'''ἑταῖρα, -ίρας (nom commun) (f)''' : compagne.<br>
'''ἑταιρείη, -ης (nom commun) (f)''' : Forme ionienne de ''ἑταιρεία''.<br>
'''ἑταιρεῖος, -ία, -ῖον (adjectif)''' : compagnon.<br>
'''ἑταῖρη, -ίρης (nom commun) (f)''' : Forme ionienne de ''ἑταῖρα''.<br>
'''ἑταίρησις, -ήσεως (nom commun) (f)''' : compagnie.<br>
'''ἑταῖρος, -ίρου (nom commun) (m)''' : compagnon.<br>
'''ἐτεός, -ά, -όν (adjectif)''' : .<br>
'''ἐτεῶς (adverbe)''' : .<br>
'''ἐτεώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἐτεός''.<br>
'''ἐτεώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de''ἐτεός''.<br>
'''ἔτης, -ου (nom commun) (m)''' : cousin ; voisin.<br>
'''ἔτος, -ους (nom commun) (n)''' : année.<br>
'''ϝέτος, -ους (nom commun) (n)''' : Forme ancienne de ''ἔτος''.<br>
'''ἔτυμος, -η, -ον (adjectif)''' : Vrai ; réel, véritable.<br>
'''εὐάζω (verbe)''' : .<br>
'''εὐαί (interjection)''' : .<br>
'''εὐαγγελίζω (verbe)''' : annoncer une bonne nouvelle.<br>
'''εὐαγγελισμός, -οῦ (nom commun) (m)''' : annonciation.<br>
'''εὖγμα, -ὔγματος (nom commun) (n)''' : vœu.<br>
'''εὐδαιμονία, -ας (nom commun) (f)''' : bonheur.<br>
'''εὐδαίμων, -ων, -ον (adjectif)''' : heureux.<br>
'''εὐδαιμόνως (adverbe)''' : heureusement.<br>
'''εὐδαιμονέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''εὐδαίμων''.<br>
'''εὐδαιμονέστερος, -έρα, -έρερον (adjectif)''' : Comparatif de ''εὐδαίμων''.<br>
'''εὕδω (verbe)''' : dormir.<br>
'''εὐγενής, -ής, -ές (adjectif)''' : noble.<br>
'''εὐεργεσία, -ας (nom commun) (f)''' : bénéfice.<br>
'''εὐεργέτης, -ου (nom commun) (n)''' : bienfaiteur.<br>
'''εὐεργέτις, -δος (nom commun) (f)''' : bienfaitrice.<br>
'''εὐεργετῶ (bénéfice)''' : bénéficier.<br>
'''εὐ- (préfixe)''' : bon, bien.<br>
'''εὐθέως (adverbe)''' : Directement ; immédiatement.<br>
'''εὔθυνα, -ύνης (nom commun) (f)''' : responsabilité.<br>
'''εὐθυνότης, -ητος (nom commun) (f)''' : responsabilité.<br>
'''εὐθύνω (verbe)''' : être responsable.<br>
'''εὐθύς, -εῖα, -ύ (adjectif)''' : Direct ; immédiat.<br>
'''εὐθύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''εὐθύς''.<br>
'''εὐθύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''εὐθύς''.<br>
'''εὐλάϐεια, -ας (nom commun) (f)''' : piété.<br>
'''εὐλαϐής, -ής, -ές (adjectif)''' : pieux.<br>
'''εὐλαϐικός, -ή, -όν (adjectif)''' : .<br>
'''εὐλαϐικῶς (adverbe)''' : avec piété.<br>
'''εὐλαϐῶς (adverbe)''' : pieusement.<br>
'''εὐλαλος, -ος, -ον (adjectif)''' : qui parle bien.<br>
'''εὐλογιά, -ᾶς (nom commun) (f)''' : variole.<br>
'''εὐλογία, -ας (nom commun) (f)''' : louange.<br>
'''εὐμετάϐλητος, -η, -ον (adjectif)''' : .<br>
'''εὐμορφία, -ας (nom commun) (f)''' : beauté.<br>
'''εὔμορφος, -ος, -ον (adjectif)''' : beau.<br>
'''εὔνοια, -ίας (nom commun) (f)''' : faveur.<br>
'''εὐνοϊκός, -ή, -όν (adjectif)''' : favorable.<br>
'''εὐνοϊκῶς (adverbe)''' : favorablement.<br>
'''εὐνοϊκώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''εὐνοϊκός''.<br>
'''εὐνοϊκώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''εὐνοϊκός''.<br>
'''εὐνοϊκώτατα, -, - (adverbe)''' : Superlatif de ''εὐνοϊκῶς''.<br>
'''εὐνοϊκώτερον, -, - (adverbe)''' : Comparatif de ''εὐνοϊκῶς''.<br>
'''εὔνους, -ους, -ουν (adjectif)''' : .<br>
'''εὐπατρίδης, -ου (nom commun) (m)''' : gentilhomme.<br>
'''εὐοπλέω (verbe)''' : être bien armé.<br>
'''εὐρετήριον, -ήριου (nom commun) (n)''' : index (liste).<br>
'''εὐρέως (adverbe)''' : largement.<br>
'''εὐρύς, -εῖα, -ύ (adjectif)''' : Large. (Par extension) Vaste, spacieux.<br>
'''εὐρύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''εὐρύς''.<br>
'''εὐρύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''εὐρύς''.<br>
'''εὔρωστος, -ος, -ον (adjectif)''' : .<br>
'''εὐρωστότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''εὔρωστος''.<br>
'''εὐρωστότερος, -έρα, -ότερον (adjectif)'''' : Comparatif de ''εὔρωστος''.<br>
'''εὐρωστότατα, -, - (adverbe)''' : Superlatif de ''εὐρώστως''.<br>
'''εὐρωστότερον, -, - (adverbe)''' : Comparatif de ''εὐρώστως''.<br>
'''εὐρώστως (adverbe)''' : .<br>
'''εὐσέϐεια, -ίας (nom commun) (f)''' : piété.<br>
'''εὐσεϐέστατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''εὐσεϐής''.<br>
'''εὐσεϐέστερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''εὐσεϐής''.<br>
'''εὐσεϐής, -ής, -ές (adjectif)''' : pieux.<br>
'''εὐσεϐῶς (adverbe)''' : pieusement.<br>
'''εὐσυνείδητος, -ος, -ον (adjectif)''' : consciencieux.<br>
'''ἐύς, -εῖα, -ύ (adjectif)''' : bon, brave.<br>
'''εὐτελέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''εὐτελής''.<br>
'''εὐτελέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''εὐτελής''.<br>
'''εὐτελής, -ής, -ές (adjectif)''' : Bon marché. De vil prix, de peu de valeur. Avare.<br>
'''εὐτελίζω (verbe)''' : mépriser.<br>
'''εὐτελῶς (adverbe)''' : .<br>
'''εὐτυχία, -ας (nom commun) (f)''' : bonheur.<br>
'''εὖ (adverbe)''' : Bien (idée d’origine) Noblement. Bien, régulièrement ; justement. Bien, avec bienveillance. Heureusement.<br>
'''εὐτυχέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''εὐτυχής''.<br>
'''εὐτυχέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''εὐτυχής''.<br>
'''εὐτυχής, -ής, -ές (adjectif)''' : heureux.<br>
'''εὐτυχῶς (adverbe)''' : heureusement.<br>
'''εὔφλεκτος, -ος, -ον (adjectif)''' : inflammable.<br>
'''εὐφραδέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''εὐφραδής''.<br>
'''εὐφραδέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''εὐφραδής''.<br>
'''εὐφραδής, -ής, -ές (adjectif)''' : .<br>
'''εὔφρων, -ων, -ον (adjectif)''' : .<br>
'''εὐφυέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''εὐφυής''.<br>
'''εὐφυέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''εὐφυής''.<br>
'''εὐφυής, -ής, -ές (adjectif)''' : .<br>
'''εὐφυῶς (adverbe)''' : .<br>
'''εὐφωνία, -ας (nom commun) (f)''' : bonne sonorité.<br>
'''εὐχαριστία, -ας (nom commun) (f)''' : .<br>
'''εὐχάριστος, -ος, -ον (adjectif)''' : .<br>
'''εὐχαριστῶ (verbe)''' : remercier.<br>
'''εὐχή, -ῆς (nom commun) (f)''' : vœu.<br>
'''εὔχομαι (verbe)''' : formuler un vœu.<br>
'''εὖχος, -ὔχους (nom commun) (n)''' : Prière, vœu.<br>
'''εὐχωλή, -ῆς (nom commun) (f)''' : Prière, vœu.<br>
'''-εύς, -έως (suffixe) (m)''' : .<br>
'''-εύω (suffixe)''' : .<br>
'''ἐφαμέριος, -ίου (nom commun) (m)''' : Forme dorienne de ''ἐφημέριος''.<br>
'''ἐφαμερεύω (verbe)''' : Forme dorienne de ''ἐφημερεύω''.<br>
'''ἐφαρμόζω (verbe)''' : appliquer.<br>
'''ἐφεξείης (adverbe)''' : Forme poétique de ''ἐφεξῆς''.<br>
'''ἐφεξῆς (adverbe)''' : dorénavant, désormais.<br>
'''ἐφήϐαιον, -ίου (nom commun) (m)''' : pubis.<br>
'''ἐφηϐεία, -ας (nom commun) (f)''' : Service militaire rassemblant les jeunes Athéniens âgés de 18 à 20 ans.<br>
'''ἐφηϐικός, -ή, -όν (adjectif)''' : adolescent.<br>
'''ἔφηϐος, -ήϐου (nom commun) (m)''' : Jeune garçon ayant quitté l’autorité des femmes.<br>
'''ἐφημέριος, -ίου (nom commun) (m)''' : curé.<br>
'''ἐφημερεύω (verbe)''' : .<br>
'''ἐφιάλτης, -ου (nom commun) (m)''' : cauchemar.<br>
'''ἔχθος, -ους (nom commun) (n)''' : Haine ; hostilité.<br>
'''ἐχθρά, -ᾶς (nom commun) (f)''' : ennemie.<br>
'''ἐχθρός, -ά, -όν (adjectif)''' : Détesté de ; ennemi de.<br>
'''ἐχθρός, -οῦ (nom commun) (m)''' : ennemi.<br>
'''ἐχθρότης, -τος (nom commun) (f)''' : inimitié.<br>
'''ἔχθω (verbe)''' : Haïr ; être hostile.<br>
'''ἔχιδνα, -ης (nom commun) (f)''' : vipère.<br>
'''ἐχῖνος, -ίνου (nom commun) (m)''' : hérisson.<br>
'''ἔχις, -εως (nom commun) (m)''' : vipère.<br>
'''ἔχω (verbe)''' : avoir.<br>
'''ἕψημα, -ήματος (nom commun) (f)''' : cuisson.<br>
'''ἕψησις, -ήσεως (nom commun) (f)''' : ébulition.<br>
'''ἒ ψιλόν (nom commun) (n)''' : epsilon.<br>
'''ἕψω (verbe)''' : Bouillir, cuire. Fondre, en parlant des métaux, raffiner, purifier. (Figuré) Être chaud pour ; chérir.<br>
'''ἕως (conjonction)''' : jusqu'à ce que ; tant que.<br>
'''Ἑϐραία, -ας (nom commun) (f)''' : Israélite.<br>
'''Ἑϐραῖος, -ίου (nom commun) (m)''' : Israélite.<br>
'''Ἑϐραΐς, -δος (nom commun) (f)''' : Israélite.<br>
'''Ἐδέμ (nom propre) (m)''' : Éden.<br>
'''Εἰλείθυια, -ίας (nom propre) (f)''' : [[wikt:Ilithyie|Ilithyie]] (déesse de l'enfantement).<br>
'''Εἰρηναῖος, -ίου (nom propre) (m)''' : Irénée.<br>
'''Εἰρήνη, -ης (nom propre) (f)''' : [[wikt:Eiréné|Eiréné]].<br>
'''Ἑκάϐη, -ης (nom propre) (f)''' : Hécube.<br>
'''Ἑκάτη, -ης (nom propre) (f)''' : Hécate.<br>
'''Ἑκατόγχειρες, -ων (nom propre) (m)''' : Hécatonchires.<br>
'''Ἕκτωρ, -ορος (nom propre) (m)''' : Hector.<br>
'''Ἐλεάζαρος, -άρου (nom propre) (m)''' : Éléazar.<br>
'''Ἐλαμείτης, -ου (nom commun) (m)''' : Élamite.<br>
'''Ἐλεύθυια, -ίας (nom propre) (f)''' : Forme ancienne de ''Εἰλείθυια''.<br>
'''Ἑλένη, -ης (nom propre) (f)''' : Hélène.<br>
'''Ἕλενος, -ένου (nom propre) (m)''' : Hélénos.<br>
'''Ἐλισάϐετ (nom propre) (f)''' : Élisabeth.<br>
'''Ἑλλάς, -δος (nom propre) (f)''' : Grèce.<br>
'''Ἕλλη, -ης (nom propre) (f)''' : Hellé.<br>
'''Ἕλλην, -ος (nom propre) (m)''' : Hellen.<br>
'''Ἕλλην, -ος (nom commun) (m)''' : Grec.<br>
'''Ἑλληνίς, -δος (nom commun) (f)''' : Grecque.<br>
'''Ἑλλήσποντος, -όντου (nom commun) (m)''' : Helléspont.<br>
'''Ἐμπεδοκλῆς, -έους (nom propre) (m)''' : Empédocle.<br>
'''Ἐνάρετη, -ης (nom propre) (f)''' : Énarété.<br>
'''Ἐνδυμίων, -ωνος (nom propre) (m)''' : Endymion.<br>
'''Ἐνετός, -οῦ (nom commun) (m)''' : Vénète.<br>
'''Ἐνυώ, -οῦς (nom propre) (f)''' : [[wikt:Ényo|Ényo]].<br>
'''Ἐπαμεινώνδας, -ου (nom commun) (m)''' : Épaminondas.<br>
'''Ἐπίκτητος, -ήτου (nom propre) (m)''' : Épictète.<br>
'''Ἐπιμηθεύς, -έως (nom propre) (m)''' : Épiméthée.<br>
'''Ἐπιφί (nom propre) (m)''' : Epiphi.<br>
'''Ἐρατοσθένης, -ους (nom propre) (m)''' : Ératosthène. (Savant grec né en -276 et mort en -194.)<br>
'''Ἐρατώ, -οῦς (nom propre) (f)''' : Érato.<br>
'''Ἔρεϐος, -έϐους (nom propre) (n)''' : [[wikt:Érèbe|Érèbe]].<br>
'''Ἐρεχθεύς, -έως (nom propre) (m)''' : Érechtée.<br>
'''Ἐρινύς, -ος (nom propre) (f)''' : Érynie.<br>
'''Ἔρις, -εως (nom propre) (f)''' : [[wikt:Éris|Éris]].<br>
'''Ἐριφύλη, -ης (nom propre) (f)''' : Ériphyle.<br>
'''Ἑρμᾶς, -οῦ (nom propre) (m)''' : Forme dorienne de ''Ἑρμῆς''.<br>
'''Ἑρμαφρόδιτος, -ίτου (nom propre) (m)''' : Hermaphrodite.<br>
'''Ἑρμῆς, -οῦ (nom propre) (m)''' : [[wikt:Hermès|Hermès]].<br>
'''Ἑρμιονεύς, -έως (nom propre) (m)''' : Hermionée.<br>
'''Ἑρμιόνη, -ης (nom propre) (f)''' : Hermione.<br>
'''Ἑρμιονίς, -δος (nom propre) (f)''' : Hermionide.<br>
'''Ἑρμιών, -ῶνος (nom propre) (m)''' : Hermion.<br>
'''Ἑρμογένης, -ους (nom propre) (m)''' : Hermogénès.<br>
'''Ἐρυσίχθων, -ονος (nom propre) (m)''' : Érysichthon.<br>
'''Ἔρως, -τος (nom propre) (m)''' : [[wikt:Éros|Éros]].<br>
'''Ἑσπερία, -ας (nom propre) (f)''' : Hespérie.<br>
'''Ἑσπερίδες, -ων (nom propre) (f)''' : Hespérides.<br>
'''Ἑσπερίς, -δος (nom propre) (f)''' : Hespéris.<br>
'''Ἕσπερος, -έρου (nom propre) (m)''' : Hespéros.<br>
'''Εὔα, -ας (nom propre) (f)''' : Ève.<br>
'''Εὐάγριος, -ίου (nom propre) (m)''' : Évagre.<br>
'''Εὐγενία, -ας (nom propre) (f)''' : Eugénie.<br>
'''Εὐγένιος, -ίου (nom propre) (m)''' : Eugène.<br>
'''Εὔδημος, -ήµου (nom propre) (m)''' : Eudème.<br>
'''Εὔδων, - (nom propre) (m)''' : Eudon (fleuve de Phrygie).<br>
'''Εὔιος, -ίου (nom propre) (m)''' : Euïos.<br>
'''Εὐλαλία, -ας (nom propre) (f)''' : Eulalie.<br>
'''Εὐλάλιος, -ίου (nom propre) (m)''' : Eulalios.<br>
'''Εὔμαιος, -ίου (nom propre) (m)''' : Eumée. (Porcher de Laërte et d’Ulysse.)<br>
'''Εὐήμερος, -έρου (nom propre) (m)''' : Évhémère.<br>
'''Εὔμηλος, -ήλου (nom propre) (m)''' : Eumélos.<br>
'''Εὔνηος, -ήου (nom propre) (m)''' : Eunée.<br>
'''Εὐνομία, -ας (nom propre) (f)''' : Eunomie.<br>
'''Εὐήρης, -ου (nom propre) (m)''' : Évérès.<br>
'''Εὐριπίδης, -ου (nom propre) (m)''' : Euripide.<br>
'''Εὔρος, -ου (nom propre) (m)''' : Euros. (dieu du vent d'est)<br>
'''Εὐρυάλη, -ης (nom propre) (f)''' : Euryale.<br>
'''Εὐρυδίκη, -ης (nom propre) (f)''' : Eurydice.<br>
'''Εὐρύθεμις, -έμιδας (nom propre) (f)''' : Eurythémis.<br>
'''Εὐρύκλεια, -ίας (nom propre) (f)''' : Euryclée.<br>
'''Εὐρύλοχος, -όχου (nom propre) (m)''' : Euryloque.<br>
'''Εὐρυσθεύς, -έως (nom propre) (m)''' : Eurysthée (fils de Sthénélos et Nicipée).<br>
'''Εὐρωπαῖος, -ίου (nom commun) (m)''' : Européen.<br>
'''Εὐρώπη, -ης (nom propre) (f)''' : Europe.<br>
'''Εὐσέϐεια, -ας (nom propre) (f)''' : Eusébie.<br>
'''Εὐσέϐιος, -ίου (nom propre) (m)''' : Eusèbe.<br>
'''Εὐστάθιος, -ίου (prénom) (m)''' : Eustache.<br>
'''Εὔσταχυς, -άχυος (prénom) (m)''' : Eustache.<br>
'''Εὐτέρπη, -ης (nom propre) (f)''' : Euterpe.<br>
'''Εὐτύχιος, -ίου (nom propre) (m)''' : Eutychius.<br>
'''Εὐφημία, -ας (nom propre) (f)''' : Euphémie.<br>
'''Εὔφημος, -ήμου (nom propre) (m)''' : Euphémos.<br>
'''Εὐφράτης, -ου (nom propre) (m)''' : Euphrate (fleuve de Turquie et d’Irak).<br>
'''Εὐφροσύνη, -ης (nom propre) (f)''' : Euphrosyne.<br>
'''Εὔφρων, -ονος (nom propre) (m)''' : Euphron.<br>
'''Ἔφεσος, -έσου (nom propre) (f)''' : Éphèse.<br>
'''Ἐφιάλτης, -ου (nom propre) (m)''' : Éphialtès.<br>
'''Ἕως, -ω (nom propre) (f)''' : Forme alternative de ''Ἠώς''.<br>
'''Ἑωσφόρος, -ου (nom propre) (m)''' : Lucifer.<br>
==Ζ==
'''ζῆλος, -ήλου (nom commun) (m)''' : Jalousie ; zèle.<br>
'''ζηλοτυπία, -ας (nom commun) (m)''' : jalousie.<br>
'''ζηλότυπος, -ος, -ον (adjectif)''' : jaloux.<br>
'''ζηλωτής, -οῦ (nom commun) (m)''' : zélote.<br>
'''ζηλῶ (verbe)''' : aimer ardemment.<br>
'''ζῆτα (nom commun) (n)''' : zêta.<br>
'''ζήτησις, -ήσεως (nom commun) (f)''' : recherche.<br>
'''ζητητικός, -ός, -όν (adjectif)''' : aimant chercher ; rechercher.<br>
'''ζητῶ (verbe)''' : demander ; chercher.<br>
'''ζιγγίϐερις, -έρεως (nom commun) (f)''' : gingembre.<br>
'''ζιζάνιον, -ίου (nom commun) (n)''' : ivraie.<br>
'''ζιζανοσπορεύς, -έως (nom commun) (m)''' : .<br>
'''ζιζανιοσπόρος, -ου (nom commun) (m)''' : .<br>
'''ζιζανώδης, -ης, -ες (adjectif)''' : .<br>
'''ζίζυφον, -ύϕου (nom commun) (n)''' : jujubier.<br>
'''ζόα, -ας (nom commun) (f)''' : Forme dorienne de ''ζωή''.<br>
'''ζόη, -ης (nom commun) (f)''' : Forme ionienne de ''ζωή''.<br>
'''ζοΐα, -ας (nom commun) (f)''' : Forme éolienne de ''ζωή''.<br>
'''ζόρξ, -κος (nom commun) (m)''' : Forme de ''δορκάς''.<br>
'''ζυγαριά, -ᾶς (nom commun) (f)''' : balance.<br>
'''ζυγόν, -οῦ (nom commun) (n)''' : joug.<br>
'''ζῦθος, -ύθου (nom commun) (m)''' : bière.<br>
'''ζωά, -ᾶς (nom commun) (f)''' : Autre forme dorienne de ''ζωή''.<br>
'''ζωή, -ῆς (nom commun) (f)''' : vie.<br>
'''ζῶμα, -ώματος (nom commun) (n)''' : caleçon.<br>
'''ζώμευμα, -ύματος (nom commun) (n)''' : soupe.<br>
'''ζωμεύω (verbe)''' : bouillir en soupe.<br>
'''ζωμήρυσις, -ύσεως (nom commun) (f)''' : louche à soupe.<br>
'''ζωμίδιον, -ίου (nom commun) (n)''' : sauce.<br>
'''ζωμός, -οῦ (nom commun) (m)''' : bouillon, soupe.<br>
'''ζωός, -ή, -όν (adjectif)''' : vivant.<br>
'''ζῷον, -ῴου (nom commun) (n)''' : animal.<br>
'''ζωοπανήγυρις, -ύρεως (nom commun) (f)''' : .<br>
'''ζῶ (verbe)''' : vivre.<br>
'''Ζαϐουλών (nom propre) (m)''' : Zabulon.<br>
'''Ζαγρεύς, -έως (nom propre) (m)''' : Zagreus.<br>
'''Ζακχαῖος, -ίου (nom propre) (m)''' : Zachée.<br>
'''Ζάν, -ός (nom propre) (m)''' : Forme dorienne de ''Ζεύς''.<br>
'''Ζάς, -νος (nom propre) (m)''' : Autre forme dorienne de ''Ζεύς''.<br>
'''Ζαχαρίας, -ου (nom propre) (m)''' : Zacharie.<br>
'''Ζεϐεδαῖος, -ίου (nom propre) (m)''' : Zébédée.<br>
'''Ζευξίππη, -ης (nom propre) (f)''' : Zeuxippe.<br>
'''Ζεῦξις, -ύξεως (nom propre) (m)''' : Zeuxis.<br>
'''Ζεῦς, -ύσεως (nom propre) (m)''' : Forme lesbienne de ''Ζεύς''.<br>
'''Ζεύς, Διός (nom propre) (m)''' : [[wikt:Zeus|Zeus]].<br>
'''Ζέφυρος, -ύρου (nom propre) (m)''' : [[wikt:Zéphyr|Zéphyr]]. (dieu du vent d'ouest)<br>
'''Ζῆλος, -ήλου (nom propre) (m)''' : Zélos.<br>
'''Ζήν, -ός (nom propre) (m)''' : Forme poétique de ''Ζεύς''.<br>
'''Ζηνοϐία, -ας (nom propre) (f)''' : Zénobie.<br>
'''Ζηνοϐίος, -ίου (nom propre) (m)''' : Zénobios.<br>
'''Ζυγός, -οῦ (nom propre) (m)''' : Balance.<br>
'''Ζωροάστρης, -ου (nom propre) (m)''' : Zoroastre.<br>
==Η==
'''ἡ (article défini)''' : la.<br>
'''ἤ (conjonction)''' : ou.<br>
'''ᾗ (adverbe relatif)''' : par là où (je passe).<br>
'''ἥϐη, -ης (nom commun) (f)''' : jeunesse.<br>
'''ἡϐητέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἡϐητής''.<br>
'''ἡϐητέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''ἡϐητής''.<br>
'''ἡϐητής, -ής, -ές (adjectif)''' : juvénile.<br>
'''ἡϐητῶς (adverbe)''' : juvénilement.<br>
'''ἡϐός, -ή, -όν (adjectif)''' : jeune.<br>
'''ἡϐότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἡϐός''.<br>
'''ἡϐότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἡϐός''.<br>
'''ἡγείσθαι (verbe)''' : conduire.<br>
'''ἡγεμονία, -ας (nom commun) (f)''' : Action de conduire, de diriger. Suprématie, puissance. Règne.<br>
'''ἡγεμονεύω (verbe)''' : .<br>
'''ἡγεμών, -όνος (nom commun) (m)''' : souverain.<br>
'''ἡγέομαι (verbe)''' : Marcher devant. (après Homère) Croire, penser.<br>
'''ἡγέτης, -ου (nom commun) (m)''' : leader.<br>
'''-ηγός, -οῦ (suffixe) (m/f)''' : .<br>
'''ἥδομαι (verbe)''' : avoir plaisir à, se réjouir de.<br>
'''ἡδονή, -ῆς (nom commun) (f)''' : plaisir.<br>
'''ἡδύς, -εῖα, -ύ (adjectif)''' : doux, agréable.<br>
'''ἡδύχρουν, - (nom commun) (n)''' : .<br>
'''ἠθικός, -ή, -όν (adjectif)''' : moral.<br>
'''ἠθικότατα, -, - (adverbe)''' : Superlatif de ''ἠθικῶς''.<br>
'''ἠθικότερον, -, - (adverbe)''' : Comparatif de ''ἠθικῶς''.<br>
'''ἠθικῶς (adverbe)''' : moralement.<br>
'''ἠθικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἠθικός''.<br>
'''ἠθικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἠθικός''.<br>
'''ἦθος, ἤθους (nom commun) (n)''' : us et coutumes.<br>
'''ἠϊθέη, -ης (nom commun) (f)''' : célibataire.<br>
'''ἠΐθεος, -έου (nom commun) (m)''' : célibataire.<br>
'''ἠι (adverbe)''' : Forme béotienne de ''ἀεί''.<br>
'''-ήϊον, -ΐου (suffixe)''' : Forme ionienne de ''-εῖον''.<br>
'''ἥκω (verbe)''' : être présent, être là.<br>
'''ἠλακάτη, -ης (nom commun) (f)''' : quenouille.<br>
'''ἤλεκτρον, -έκτρου (nom commun) (n)''' : ambre jaune.<br>
'''ἠλέκτωρ, -, - (adjectif)''' : brillant.<br>
'''ἡλιακός, -ή, -όν (adjectif)''' : solaire.<br>
'''ἤλιθιος, -η, -ον (adjectif)''' : stupide.<br>
'''ἡλικία, -ας (nom commun) (f)''' : âge.<br>
'''ἥλιος, -ίου (nom commun) (m)''' : soleil.<br>
'''ἠέλιος, -ίου (nom commun) (m)''' : Forme homérique de ''ἥλιος''.<br>
'''ἡλιοστάσιον, -ίου (nom commun) (n)''' : solstice.<br>
'''ἧλιξ, ἥλικος (nom commun) (m/f)''' : camarade.<br>
'''ἧλος, ἥλου (nom commun) (m)''' : Clou, tête de clou. Cal, durillon, galle.<br>
'''ἧμαι (verbe)''' : être assis.<br>
'''ἡμεῖς (pronom personnel) (m)''' : nous.<br>
'''ἡμέρα, -ας (nom commun) (f)''' : jour.<br>
'''ἡμέρα Ἡλίου (nom commun) (f)''' : dimanche.<br>
'''ἡμέρα Σελήνης (nom commun) (f)''' : lundi.<br>
'''ἡμέρα Ἄρεως (nom commun) (f)''' : mardi.<br>
'''ἡμέρα Ἕρμου (nom commun) (f)''' : mercredi.<br>
'''ἡμέρα Διός (nom commun) (f)''' : jeudi.<br>
'''ἡμέρα Ἀφροδίτης (nom commun) (f)''' : vendredi.<br>
'''ἡμέρα Κρόνου (nom commun) (f)''' : samedi.<br>
'''ἡμέρη, -ης''' (nom commun) (f) Forme homérique et ionienne de ''ἡμέρα''.<br>
'''ἡμερήσιος, -ια, -ον (adjectif)''' : journalier ; diurne.<br>
'''ἡμερολόγιον, -ίου (nom commun) (n)''' : calendrier.<br>
'''ἡμέτερος, -έρα, -έτερον (adjectif possessif)''' : notre.<br>
'''ἡμι- (préfixe)''' : à moitié.<br>
'''ἡμίκλαστος, -, - (adjectif)''' : .<br>
'''ἡμικρανία, -ας (nom commun) (f)''' : migraine.<br>
'''ἡμίονος, -όνου (nom commun) (m)''' : hémione.<br>
'''ἡνία, -ας (nom commun) (f)''' : .<br>
'''ἡνίοχος, -όχου (nom commun) (m)''' : cocher.<br>
'''ἧπαρ, ἥπατος (nom commun) (n)''' : foie.<br>
'''ἡπατικός, -ή, -όν (adjectif)''' : hépatique.<br>
'''ἡπατῖτις, -ίτης (nom commun) (f)''' : hépatite.<br>
'''ἤπειρος, -ίρου (nom commun) (f)''' : continent.<br>
'''ἠπειρωτικός, -ή, -όν (adjectif)''' : continental.<br>
'''ἡράκλειος, -α, -ον (adjectif)''' : herculéen.<br>
'''ἡρωίνη, -ης (nom commun) (f)''' : héroïne.<br>
'''ἡρωικός, -ή, -όν (adjectif)''' : héroïque.<br>
'''ἡρωικότατα, -, - (adverbe)''' : Superlatif de ''ἡρωικῶς''.<br>
'''ἡρωικότερον, -, - (adverbe)''' : Comparatif de ''ἡρωικῶς''.<br>
'''ἡρωικῶς (adverbe)''' : héroïquement.<br>
'''ἡρωικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἡρωικός''.<br>
'''ἡρωικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἡρωικός''.<br>
'''ἡρωισμός, -οῦ (nom commun) (m)''' : héroïsme.<br>
'''ἡρωίς, -δος (nom commun) (f)''' : héroïne.<br>
'''ἥρως, -ος (nom commun) (m)''' : héros.<br>
'''ἧσσα, ἥσσης (nom commun) (f)''' : défaite.<br>
'''ἡσσῶμαι (verbe)''' : être défait.<br>
'''ἡσυχάζω (verbe)''' : être en paix, garder le silence.<br>
'''ἡσυχασμός, -οῦ (nom commun) (m)''' : hésychasme.<br>
'''ἡσυχία, -ας (nom commun) (f)''' : Immobilité. Repos, calme ; silence.<br>
'''ἥσυχος, -ος, -ον (adjectif)''' : Calme, tranquille.<br>
'''ἡσύχως (adverbe)''' : Calmement, tranquillement.<br>
'''ἡσυχώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἥσυχος''.<br>
'''ἡσυχώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἥσυχος''.<br>
'''σωματικώτατα, -, - (adverbe)''' : Superlatif de ''ἡσύχως''.<br>
'''σωματικώτερον, -, - (adverbe)''' : Comparatif de ''ἡσύχως''.<br>
'''ἦτα (nom commun) (n)''' : êta.<br>
'''ἧτρον, ἥτρου (nom commun) (n)''' : abdomen.<br>
'''ἧττα, ἥττης (nom commun) (f)''' : Forme attique de ''ἧσσα''.<br>
'''ἡττῶμαι (verbe)''' : Forme attique de ''ἡσσῶμαι''.<br>
'''ἡφαιστεῖον, -ίου (nom commun) (m)''' : volcan.<br>
'''ἦχος, ἤχου (nom commun) (m)''' : son.<br>
'''ἠχώ, -οῦς (nom commun) (f)''' : son répercuté.<br>
'''ἠώς, -οῦς (nom commun) (f)''' : aurore.<br>
'''Ἥϐη, -ης (nom propre) (f)''' : [[wikt:Hébé|Hébé]].<br>
'''Ἡγήσιππος, -ίππου (nom propre) (m)''' : Hégésippe.<br>
'''Ἡγησώ, -οῦς (nom propre) (f)''' : Hègèsô.<br>
'''Ἠλίας, -α (nom propre) (m)''' : Élie.<br>
'''Ἥλιος, -ίου (nom propre) (m)''' : [[wikt:Hélios|Hélios]].<br>
'''Ἠλύσιον, -ίου (nom propre) (n)''' : Élysée. (Séjour des hommes vertueux après leur mort.)<br>
'''Ἡμέρα, -ας (nom propre) (f)''' : [[wikt:Héméra|Héméra]].<br>
'''Ἤπειρος, -ίρου (nom propre) (f)''' : Épire.<br>
'''Ἠπιδανός, -οῦ (nom propre) (m)''' : Forme ionienne d<nowiki>'</nowiki>''Ἀπιδανός''.<br>
'''Ἥρα, -ας (nom propre) (f)''' : [[wikt:Héra|Héra]].<br>
'''Ἡρακλέης, -ους (nom propre) (m)''' : Forme poétique de ''Ἡρακλῆς''.<br>
'''Ἡρακλῆς, -έους (nom propre) (m)''' : Héraclès.<br>
'''Ἡρόστρατος, -άτου (nom propre) (m)''' : Hérostrate.<br>
'''Ἠσαΐας, -ου (nom propre) (m)''' : Isaïe.<br>
'''Ἡσαΐας, -ου (nom propre) (m)''' : Forme alternative de ''Ἠσαΐας''.<br>
'''Ἡσαῦ (nom propre) (m)''' : Ésaü.<br>
'''Ἠσαῦ (nom propre) (m)''' : Forme alternative de ''Ἡσαῦ''.<br>
'''Ἡσύχιος, -ίου (nom propre) (m)''' : Hésychios.<br>
'''Ἥφαιστος, -ίστου (nom propre) (m)''' : [[wikt:Héphaïstos|Héphaïstos]].<br>
'''Ἠχώ, -οῦς (nom propre) (f)''' : [[wikt:Écho|Écho]].<br>
'''Ἠώς, -οῦς (nom propre) (f)''' : [[wikt:Éos|Éos]].<br>
==Θ==
'''θαλάμη, -ης (nom commun) (f)''' : Gîte ; tanière.<br>
'''θάλαμος, -άμου (nom commun) (m)''' : chambre.<br>
'''θάλασσα, -ης (nom commun) (f)''' : mer.<br>
'''θάλαττα, -ης (nom commun) (f)''' : Forme attique de ''θάλασσα''.<br>
'''θάμϐος, -ους (nom commun) (n)''' : stupeur.<br>
'''θανατηφόρος, -ος, -ον (adjectif)''' : mortifère.<br>
'''θάνατος, -άτου (nom commun) (m)''' : mort.<br>
'''θανάτωσις, -ώσεως (nom commun) (f)''' : .<br>
'''θανατῶ (verbe)''' : .<br>
'''θανών, -ών, -όν (adjectif)''' : mort.<br>
'''θάπτω (verbe)''' : enterrer.<br>
'''θαυμάζω (verbe)''' : être stupéfait.<br>
'''θαῦμα, -ύατος (nom commun) (n)''' : objet d’admiration ou d’étonnement ; merveille.<br>
'''θαυμαστός, -ή, -όν (adjectif)''' : merveilleux.<br>
'''θαυματουργία, -ας (nom commun) (f)''' : tour de force ; création de merveilles.<br>
'''θαυματουργός, -ός, -όν (adjectif)''' : Qui crée des merveilles.<br>
'''θαυματουργός, -οῦ (nom commun) (m)''' : créateur de merveilles.<br>
'''θαυματουργῶ (verbe)''' : créer des merveilles.<br>
'''θεά, -ᾶς (nom commun) (f)''' : déesse.<br>
'''θέα, -ας (nom commun) (f)''' : contemplation.<br>
'''θέαμα, -άματος (nom commun) (n)''' : spectacle.<br>
'''θεατής, -οῦ (nom commun) (m)''' : spectateur.<br>
'''θεατρικός, -ή, -όν (adjectif)''' : théâtral.<br>
'''θεατρικῶς (adverbe)''' : théâtralement.<br>
'''θεατρικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''θεατρικός''.<br>
'''θεατρικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''θεατρικός''.<br>
'''θεατρικώτατα, -, - (adverbe)''' : Superlatif de ''θεατρικῶς''.<br>
'''θεατρικώτερον, -, - (adverbe)''' : Comparatif de ''θεατρικῶς''.<br>
'''θέατρον, -ου (nom commun) (n)''' : théâtre.<br>
'''θεατρινισμός, -οῦ (nom commun) (m)''' : .<br>
'''θεατρινίστικος, -η, -ον (adjectif)''' : .<br>
'''θεατρίνος, -ου (nom commun) (m)''' : acteur.<br>
'''θέημα, -ήματος (nom commun) (n)''' : Forme ionienne de ''θέαμα''.<br>
'''θέητρον, -ου (nom commun) (n)''' : Forme ionienne de ''θέατρον''.<br>
'''θεῖα, -ίας (nom commun) (f)''' : tante.<br>
'''θεῖον, -ίου (nom commun) (m)''' : soufre ; fumée de soufre.<br>
'''θεῖος, -ίου (nom commun) (m)''' : oncle.<br>
'''θέειος, -ίου (nom commun) (m)''' : Forme homérique de ''θεῖος''.<br>
'''θεήϊος, -ΐου (nom commun) (m)''' : Forme de ''θεῖος''.<br>
'''-θέτης, -ου (suffixe) (m)''' : .<br>
'''θέλγω (verbe)''' : fasciner.<br>
'''θέμα, -τος (nom commun) (n)''' : Dépôt, ce qui est placé, posé ou déposé. Endroit où l’on pose ou place quelque chose. .<br>
'''θεματίζω (verbe)''' : déposer.<br>
'''θεματίτης, -ου (nom commun) (m)''' : dépositaire.<br>
'''θεματικός, -ή, -όν (adjectif)''' : thématique.<br>
'''θέμις, -τος (nom commun) (f)''' : loi divine ou morale.<br>
'''θέογνις, -δος (nom commun) (m)''' : .<br>
'''θεός, -οῦ (nom commun) (m)''' : dieu.<br>
'''θεραπεία, -ας (nom commun) (f)''' : soin.<br>
'''θεραπευτήριον, -ίου (nom commun) (n)''' : sanatorium.<br>
'''θεραπευτής, -οῦ (nom commun) (m)''' : soignant.<br>
'''θεραπευτικός, -ή, -όν (adjectif)''' : attentif, serviable ; curatif.<br>
'''θεραπεύω (verbe)''' : soigner ; prendre soin de.<br>
'''θεράπων, -οντος (nom commun) (m)''' : serviteur.<br>
'''θερμόν, -οῦ (nom commun) (n)''' : chaleur.<br>
'''θερμός, -ή, -όν (adjectif)''' : chaud.<br>
'''θερμότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''θερμός''.<br>
'''θερμότερος, -έρη, -ότατον (adjectif)''' : Comparatif de ''θερμός''.<br>
'''θερμῶς (adverbe)''' : chaudement.<br>
'''θέρος, -ους (nom commun) (n)''' : été.<br>
'''θέρω (verbe)''' : chauffer.<br>
'''θέσις, -εως (nom commun) (f)''' : .<br>
'''θεσμός, -οῦ (nom commun) (m)''' : (Primitivement) Toute loi ou institution établie par les dieux, institution sacrée, rite, coutume antique. Loi divine ou naturelle, par opposition à la loi écrite. (Par extension) Loi faite par les hommes, loi écrite.<br>
'''θεσμοθέτης, -ου (nom commun) (m)''' : parlementaire.<br>
'''θεσμοθετεῖον, -ίου (nom commun) (n)''' : parlement.<br>
'''θεύς, -έως (nom commun) (m)''' : Forme dorienne de ''θεός''.<br>
'''θεῶμαι (verbe)''' : Regarder, contempler (le plus souvent avec étonnement ou admiration), admirer. Contempler en esprit. Voir clairement. Être spectateur. Revoir, passer en revue, examiner.<br>
'''θεώρημα, -ήματος (nom commun) (n)''' : Spectacle, fête ; règle, principe, objet d'étude ou de méditation ; contemplation, recherche.<br>
'''θεωρητικός, -ή, -όν (adjectif)''' : théorique.<br>
'''θεωρητικῶς (adverbe)''' : théoriqument.<br>
'''θεωρητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''θεωρητικός''.<br>
'''θεωρητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''θεωρητικός''.<br>
'''θεωρητικώτατα, -, - (adverbe)''' : Superlatif de ''θεωρητικῶς''.<br>
'''θεωρητικώτερον, -, - (adverbe)''' : Comparatif de ''θεωρητικῶς''.<br>
'''θεωρία, -ας (nom commun) (f)''' : Observation, vue, action de voir. Spectacle, ce qui est vu. Mission diplomatique. État de spectateur, audience.<br>
'''θεωρός, -οῦ (nom commun) (m)''' : Spectateur, observateur.<br>
'''θεωρῶ (verbe)''' : Observer, examiner, contempler. Inspecter, passer en revue. Contempler. (Figuré) Contempler par l’intelligence.<br>
'''θέω (verbe)''' : courir.<br>
'''θήϊος, -ΐου (nom commun) (m)''' : Forme éolienne de ''θεῖος''.<br>
'''θήκη, -ης (nom commun) (f)''' : Boîte, coffre. Cercueil, tombeau.<br>
'''θήλεια, - (nom commun) (f)''' : .<br>
'''θηλυκός, -ή, -όν (adjectif)''' : féminin.<br>
'''θῆλυ, -ήλεας (nom commun) (f)''' : femelle.<br>
'''θῆλυς, -ήλεια, -ῆλυ (adjectif)''' : femelle.<br>
'''θηλύτης, -τος (nom commun) (f)''' : féminité.<br>
'''θηλῶ (verbe)''' : .<br>
'''θῆμα, -ήματος (nom commun) (n)''' : tombe.<br>
'''θήρα, -ας (nom commun) (f)''' : chasse.<br>
'''θήραμα, -άματος (nom commun) (n)''' : proie.<br>
'''θηρευτής, -οῦ (nom commun) (m)''' : chasseur.<br>
'''θηρευτικός, -ή, -όν (adjectif)''' : de chasse.<br>
'''θηριότης, -τας (nom commun) (f)''' : monstruosité.<br>
'''θηριώδης, -ης, -ες (adjectif)''' : monstrueux.<br>
'''θηριωδία, -ας (nom commun) (f)''' : monstruosité.<br>
'''θηρίον, -ου (nom commun) (n)''' : animal sauvage.<br>
'''θήρ, -ός (nom commun) (m)''' : bête sauvage.<br>
'''θηρῶ (verbe)''' : chasser.<br>
'''θησαυρός, -οῦ (nom commun) (m)''' : trésor.<br>
'''θῆσσα, -ήσσα (nom commun) (f)''' : serve.<br>
'''θής, -τός (nom commun) (m)''' : serf.<br>
'''θῆτα (nom commun) (n)''' : thêta.<br>
'''θητεία, -ας (nom commun) (f)''' : service.<br>
'''θητεύω (verbe)''' : asservir.<br>
'''θῆττα, -ήττα (nom commun (f)''' : Forme attique de ''θῆσσα''.<br>
'''θιγγάνω (verbe)''' : toucher.<br>
'''θιός, -οῦ (nom commun) (m)''' : Forme béotienne et arcado-chypriote de ''θεός''.<br>
'''θλάσις, -εως (nom commun) (f)''' : contusion.<br>
'''θλιϐή, -ῆς (nom commun) (f)''' : .<br>
'''θλίϐω (verbe)''' : Presser ; compresser, réduire. Oppresser, attrister.<br>
'''θλῖψις, -ίψεως (nom commun) (f)''' : Pression, compression ; oppression.<br>
'''θλίψις, -εως (nom commun) (f)''' : chagrin, tristesse.<br>
'''θλῶ (verbe)''' : étendre.<br>
'''θναίσκω (verbe)''' : Forme éolienne de ''θνῄσκω''.<br>
'''θνᾴσκω (verbe)''' : Forme dorienne de ''θνῄσκω''.<br>
'''θνῄσκω (verbe)''' : mourir.<br>
'''θοάζω (verbe)''' : se hâter.<br>
'''θολερός, -ά, -όν (adjectif)''' : sale.<br>
'''θολερώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''θολερός''.<br>
'''θολερώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''θολερός''.<br>
'''θολερῶς (adverbe)''' : salement.<br>
'''θοός, -ή, -όν (adjectif)''' : rapide.<br>
'''θόρυϐος, -ύϐου (nom commun) (m)''' : bruit.<br>
'''θορυϐωδέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''θορυϐώδης''.<br>
'''θορυϐωδέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''θορυϐώδης''.<br>
'''θορυϐώδης, -ης, -ες (adjectif)''' : bruyant.<br>
'''θορυϐώδως (adverbe)''' : bruyamment.<br>
'''θοῶς (adverbe)''' : rapidement.<br>
'''θοώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''θοός''.<br>
'''θοώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''θοός''.<br>
'''θρᾴκιος, -ος, -ον (adjectif)''' : thrace.<br>
'''θρασέως (adverbe)''' : audacieusement.<br>
'''θράσος, -ους (nom commun) (n)''' : audace.<br>
'''θρασύς, -εῖα, -ύ (adjectif)''' : audacieux.<br>
'''θρασύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''θρασύς''.<br>
'''θρασύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''θρασύς''.<br>
'''θραῦσμα, -ύσματος (nom commun) (n)''' : fragment.<br>
'''θρασύτης, -τητος (nom commun) (f)''' : audace (qualité).<br>
'''θρῄκιος, -ος, -ον (adjectif)''' : Forme poétique de ''θρᾴκιος''.<br>
'''θρῆνος, -ήνου (nom commun) (m)''' : lamentation.<br>
'''θρηνῶ (verbe)''' : se lamenter.<br>
'''θρησκεία, -ας (nom commun) (n)''' : religion.<br>
'''θρησκεύω (verbe)''' : observer religieusement.<br>
'''θρῆσκος, -ήσκα, -ῆσκον (adjectif)''' : pieux.<br>
'''θρίαμϐος, -άμϐου (nom commun) (m)''' : triomphe.<br>
'''θρίξ, τριχός (nom commun) (f)''' : poil.<br>
'''θρίψ, -πός (nom commun) (m)''' : petite vrillette. (Au figuré) avare.<br>
'''θροέω (verbe)''' : froisser.<br>
'''θρόμϐος, -ου (nom commun) (m)''' : Grumeau, caillot.<br>
'''θρόνος, -ου (nom commun) (m)''' : siège élevé.<br>
'''θρόος, -ου (nom commun) (m)''' : froissement.<br>
'''θροῦς, -οῦ (nom commun) (m)''' : rumeur.<br>
'''θροῶ (verbe)''' : froisser.<br>
'''θρυλικός, -ή -όν (adjectif)''' : légendaire.<br>
'''θρῦλος, -ύλου (nom commun) (f)''' : légende. (Récit populaire, plus ou moins fabuleux.)<br>
'''θρύπτω (verbe)''' : casser ; morceler.<br>
'''θύαρος, -άρου (nom commun) (m)''' : ivraie.<br>
'''θυγάτηρ, -ρός (nom commun) (f)''' : fille. (descendante)<br>
'''θύλακος, -άκου (nom commun) (m)''' : bourse ; sac.<br>
'''θῦμα, -ύματος (nom commun) (n)''' : victime.<br>
'''θυμίαμα, -άματος (nom commun) (n)''' : encens.<br>
'''θυμιάω (verbe)''' : brûler.<br>
'''θυμίημα, -ήματος (nom commun) (n)''' : Forme ionienne de ''θυμίαμα''.<br>
'''θυμολέων, -ων, -ον (adjectif)''' : Courageux ; cœur-de-lion.<br>
'''θυμός, -οῦ (nom commun) (m)''' : Âme, souffle de vie. Esprit, cœur. Courage.<br>
'''θύμος, -ου (nom commun) (m)''' : thym.<br>
'''θύννος, -ου (nom commun) (m)''' : thon.<br>
'''θύρα, -ας (nom commun) (f)''' : Porte, battant ; ouverture, entrée.<br>
'''θυραωρός, -οῦ (nom commun) (m)''' : Forme homérique de ''θυρωρός''.<br>
'''θυρωρεῖον, -ίου (nom commun) (n)''' : conciergerie.<br>
'''θυρωρός, -οῦ (nom commun (m) : concierge.<br>
'''θύρσος, -ου (nom commun) (m)''' : thyrse.<br>
'''θύρωμα, -ώματος (nom commun) (n)''' : portail.<br>
'''θυσία, -ας (nom commun) (f)''' : sacrifice.<br>
'''θυσιάζω (verbe)''' : sacrifier.<br>
'''θωή, -ῆς (nom commun) (f)''' : .<br>
'''θῶμα, -ώματος (nom commun) (n)''' : Forme ionienne de ''θαῦμα''.<br>
'''θωμαστός, -ή, -όν (adjectif)''' : Forme ionienne de ''θαυμαστός''.<br>
'''θῶμιγξ, -ώμιγγος (nom commun) (f)''' : Corde ; ficelle.<br>
'''θωπεία, -ας (nom commun) (f)''' : caresse.<br>
'''θώπευμα, -ύματος (nom commun) (n)''' : caresse.<br>
'''θωπευτικός, -ή, -όν (adjectif)''' : caressant.<br>
'''θωπεύω (verbe)''' : caresser.<br>
'''θώς, -ός (nom commun) (m/f)''' : chacal.<br>
'''θώϋμα, -τος (nom commun) (n)''' : Autre forme ionienne de ''θαῦμα''.<br>
'''θώψ, -πός (nom commun) (m)''' : adulateur.<br>
'''Θαδδαῖος, -ίου (nom propre) (m)''' : Thadée.<br>
'''Θαΐς, -δος (nom propre) (f)''' : [[wikt:Thaïs|Thaïs]].<br>
'''Θάλεια, -ας (nom propre) (f)''' : Thalie. (muse)<br>
'''Θαλῆς, -άλεω (nom propre) (m)''' : Thalès.<br>
'''Θαλία, -ας (nom propre) (f)''' : Thalie. (Charite)<br>
'''Θάνατος, -άτου (nom propre) (m)''' : [[wikt:Thanatos|Thanatos]].<br>
'''Θαῦμας, -ύμαντος (nom propre) (m)''' : Thaumas.<br>
'''Θάψος, -ου (nom propre) (f)''' : Thapsus. (ville de Tunisie)<br>
'''Θέμις, -δος (nom propre) (f)''' : [[wikt:Thémis|Thémis]].<br>
'''Θεμιστοκλῆς, -έους (nom propre) (m)''' : Thémistocle.<br>
'''Θέτις, -δος (nom propre) (f)''' : [[wikt:Thétis|Thétis]].<br>
'''Θέογνις, -δος (nom propre) (m)''' : Théognis.<br>
'''Θεοδοσία, -ας (nom propre) (f)''' : Théodosia.<br>
'''Θεοδόσιος, -ίου (nom propre) (m)''' : Théodose.<br>
'''Θεόδουλος, -ύλος (nom propre) (m)''' : Théodule.<br>
'''Θεόδωρος, -ώρου (nom propre) (m)''' : Théodore.<br>
'''Θεόπομπος, -όμπου (nom propre) (m)''' : Théopompe.<br>
'''Θεός, -οῦ (nom propre) (m)''' : Dieu.<br>
'''Θεοφάνης, -ου (nom propre) (m)''' : Théophane.<br>
'''Θεοφανία, -ας (nom propre) (f)''' : Théophanie.<br>
'''Θεόφιλος, -ίλου (nom propre) (m)''' : Théophile.<br>
'''Θεόφραστος, -άστου (nom propre) (m)''' : Théophraste.<br>
'''Θερμοπύλαι, -ῶν (nom propre) (f)''' : Thermopyles.<br>
'''Θέσπις, -εως (nom propre) (m)''' : Thespsis.<br>
'''Θεσσαλία, -ας (nom propre) (f)''' : Thessalie.<br>
'''Θεσσαλικός, -ή, -όν (adjectif)''' : thessalien.<br>
'''Θεσσαλίς, -δος (nom commun) (f)''' : Thessalienne.<br>
'''Θεσσαλονίκη, -ης (nom propre) (f)''' : Thessalonique.<br>
'''Θεσσαλός, -οῦ (nom commun) (m)''' : Thessalien.<br>
'''Θευδᾶς, -ᾶ (nom propre) (m)''' : Theudas.<br>
'''Θευδέριχος, -ίχου (nom propre) (m)''' : Théodoric.<br>
'''Θῆϐαι, -ῶν (nom propre) (f)''' : Thèbes.<br>
'''Θησεύς, -έως (nom propre) (m)''' : Thésée.<br>
'''Θίνη, -ης (nom propre) (f)''' : Synonyme de ''Σίνη''.<br>
'''Θιός, -οῦ (nom propre) (m)''' : Forme béotienne de ''Ζεύς''.<br>
'''Θίσϐη, -ης (nom propre) (f)''' : Thisbé.<br>
'''Θόας, -αντος (nom propre) (m)''' : Thoas.<br>
'''Θούθμωσις, -ώσιδος (nom propre) (m)''' : Thoutmôsis.<br>
'''Θουκυδίδης, -ου (nom propre) (m)''' : Thucydide.<br>
'''Θόωσα, -ης (nom propre) (f)''' : Thoosa.<br>
'''Θρᾴκη, -ης (nom propre) (f)''' : Thrace.<br>
'''Θρᾷξ, -ᾳκός (nom propre) (m)''' : Thrax.<br>
'''Θρᾷξ, -ᾳκός (nom commun) (m)''' : Thrace.<br>
'''Θρᾷσσα, -ᾴσσης (nom commun) (f)''' : Thrace.<br>
'''Θρᾷττα, -ᾴττης (nom commun) (f)''' : Forme attique de ''Θρᾷσσα''.<br>
'''Θρῄκη, -ης (nom propre) (f)''' : Forme poétique de ''Θρᾴκη''.<br>
'''Θρῇξ, -ῃκός (nom commun) (m)''' : Forme poétique de ''Θρᾷξ''.<br>
'''Θρῇσσα, -ῄσσης (nom commun) (f)''' : Forme poétique de ''Θρᾷσσα''.<br>
'''Θωθ (nom propre) (m)''' : Thot.<br>
'''Θωμᾶς, -ᾶ (prénom) (m)''' : Thomas.<br>
==Ι==
'''ἴαμϐος, -άµϐου (nom commun) (m)''' : ïambe.<br>
'''ἰάομαι (verbe)''' : soigner.<br>
'''ἱαρός, -ά, -όν (adjectif)''' : Forme dorienne de ''ἱερός''.<br>
'''ἴασις, -άσεως (nom commun) (f)''' : cure ; remède.<br>
'''ἴασπις, -δος (nom commun) (f)''' : jaspe.<br>
'''ἰατήρ, -ῆρος (nom commun) (m)''' : guérisseur.<br>
'''ἰατός, -ός, -όν (adjectif)''' : curable ; soignable.<br>
'''ἰατρεία, -ας (nom commun) (f)''' : médecine.<br>
'''ἰατρικός, -ή, -όν (adjectif)''' : médical.<br>
'''ἰατρικῶς (adverbe)''' : médicalement.<br>
'''ἰατρικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἰατρικός''.<br>
'''ἰατρικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἰατρικός''.<br>
'''ἰατρικώτατα, -, - (adverbe)''' : Superlatif de ''ἰατρικῶς''.<br>
'''ἰατρικώτερον, -, - (adverbe)''' : Comparatif de ''ἰατρικῶς''.<br>
'''ἰατρός, -οῦ (nom commun) (m)''' : médecin.<br>
'''ἱϐίσκος, -ου (nom commun) (m)''' : guimauve.<br>
'''ἴγνης, -τος (nom commun) (m)''' : indigène.<br>
'''ἰγνύα, -ας (nom commun) (f)''' : jarret.<br>
'''ἰγνύη, -ης (suffixe) (f)''' : Forme hommérique et ionienne de ''ἰγνύα''.<br>
'''ἰγνύς, -ος (nom commun) (f)''' : Forme de ''''.<br>
'''ἴκτις, -δος (nom commun) (f)''' : martre.<br>
'''ἱρός, -ά, -όν (adjectif)''' : Forme ionienne de ''ἱερός''.<br>
'''ἷρος, -α, -ον (adjectif)''' : Forme éolienne de ''ἱερός''.<br>
'''-ία, -ας (suffixe) (f)''' : marque nominale.<br>
'''-ικός, -ή, -όν (suffixe)''' : marque adjectivale.<br>
'''-ικῶς (adverbe)''' : Forme adverbiale de ''-ικός''.<br>
'''-ικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''-ικός''.<br>
'''-ικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''-ικός''.<br>
'''-ικώτατα, -, - (adverbe)''' : Superlatif de ''-ικῶς''.<br>
'''-ικώτερον, -, - (adverbe)''' : Comparatif de ''-ικῶς''.<br>
'''-ίσκος, -ου (suffixe)''' : diminutif.<br>
'''-ιμος, -ος, -ον (suffixe)''' : .<br>
'''ἱαρεύς, -έως (nom commun) (m)''' : Forme dorienne de ''ἱερεύς''.<br>
'''ἰαῦ (interjection)''' : hé, ho.<br>
'''ἰαύω (verbe)''' : Dormir, se reposer.<br>
'''ἶϐις, ἴϐιδος (nom commun) (f)''' : ibis.<br>
'''ἰδίᾳ (adverbe)''' : en particulier, séparément, à part.<br>
'''ἰδιοσυγκρασία, -ας (nom commun) (f)''' : tempérament particulier.<br>
'''ἴδιος, -ία, -ιον (adjectif)''' : propre, particulier.<br>
'''ἰδιοφυής, -ής, -ές (adjectif)''' : génial.<br>
'''ἰδιοφυΐα, -ας (nom commun) (f)''' : génie (talent d’une personne).<br>
'''ἰδιώτης, -ου (nom commun) (m)''' : individu, plébéien ; ignorant.<br>
'''ἰδίω (verbe)''' : suer.<br>
'''ἵδρυμα, -ύματος (nom commun) (n)''' : institution.<br>
'''ἵδρυσις, -ύσεως (nom commun) (n)''' : fondation.<br>
'''ἱδρύω (verbe)''' : .<br>
'''ἱδρώς, -ῶτος (nom commun) (m)''' : sueur ; sudation. Sève ; jus, moisissure.<br>
'''ἱέραξ, -κου (nom commun) (m)''' : faucon.<br>
'''ἱερατεία, -ας (nom commun) (f)''' : tempérament particulier.<br>
'''ἱεράτευμα, -ύματος (nom commun) (n)''' : .<br>
'''ἱερατεύω (verbe)''' : .<br>
'''ἱερατικός, -ή, -όν (adjectif)''' : sacerdotal.<br>
'''ἱερατικῶς (adverbe)''' : sacerdotalement.<br>
'''ἱερατικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἱερατικός''.<br>
'''ἱερατικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἱερατικός''.<br>
'''ἱέρεια, -ίας (nom commun) (f)''' : prêtresse.<br>
'''ἱερεύς, -έως (nom commun) (m)''' : prêtre.<br>
'''ἱεροκῆρυξ, -ήρυκος (nom commun) (m)''' : prêcheur.<br>
'''ἱερόν, -οῦ (nom commun) (n)''' : sanctuaire.<br>
'''ἱεροσυλία, -ας (nom commun) (m)''' : sacrilège (action).<br>
'''ἱερόσυλος, -ύλου (nom commun) (m)''' : sacrilège (personne).<br>
'''ἱερός, -ά, -όν (adjectif)''' : sacré.<br>
'''ἱερῶς (adverbe)''' : sacrément.<br>
'''ἱερώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἱερός''.<br>
'''ἱερώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἱερός''.<br>
'''ἱεροσύνη, -ης (nom commun) (f)''' : sacerdoce.<br>
'''ἱερῶ (verbe)''' : .<br>
'''ἰή (interjection)''' : Youpi, hourra. Aïe.<br>
'''ἷημι (verbe)''' : envoyer, lancer.<br>
'''ἰητρός, -οῦ (nom commun) (m)''' : Forme homérique et ionienne de ''ἰατρός''.<br>
'''ἰθαγενής, -ής, -ές (adjectif)''' : indigène.<br>
'''ἰθακήσιος, -ος, -ον (adjectif)''' : ithacien.<br>
'''ἰθέως (adverbe)''' : Forme homérique et ionienne de ''εὐθέως''.<br>
'''ἰθύς, -εῖα, -ύ (adjectif)''' : Forme homérique et ionienne de ''εὐθύς''.<br>
'''ἰθύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''ἰθύς''.<br>
'''ἰθύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''ἰθύς''.<br>
'''ἰθύφαλλος, -άλλου (nom commun) (m)''' : ithyphalle.<br>
'''ἱκεσία, -ας (nom commun) (f)''' : supplique.<br>
'''ἱκετεύω (verbe)''' : supplier.<br>
'''ἱκέτις, -δος (nom commun) (f)''' : pèlerine.<br>
'''ἱκέτης, -ου (nom commun) (m)''' : pèlerin.<br>
'''ἴκκος, -ου (nom commun) (m/f)''' : Forme éolienne de ''ἵππος''.<br>
'''ἱκνέομαι (verbe)''' : aller à ; atteindre. Venir.<br>
'''ἴκρια, -ας (nom commun) (f)''' : .<br>
'''ἴκριον, -ου (nom commun) (n)''' : échafaudage.<br>
'''ἰκρίωμα, -ώματος (nom commun) (n)''' : échafaudage.<br>
'''ἰκριῶ (verbe)''' : .<br>
'''ἴκτις, -δος (nom commun) (f)''' : fouine.<br>
'''-ικός, -ή, -όν (adjectif)''' : -ique.<br>
'''-ικῶς (adverbe)''' : -ment.<br>
'''-ικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''-ικός''.<br>
'''-ικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''-ικός''.<br>
'''-ικώτατα, -, - (adverbe)''' : Superlatif de ''-ικῶς''.<br>
'''-ικώτερον, -, - (adverbe)''' : Comparatif de ''-ικῶς''.<br>
'''ἵκω (verbe)''' : venir.<br>
'''ἵλαος, -ος, -ον (adjectif)''' : propice (en parlant des dieux). Gentil, aimable (en parlant des hommes).<br>
'''ἱλαρός, -ά, -όν (adjectif)''' : joyeux, riant.<br>
'''ἱλαρότης, -τος (nom commun) (f)''' : hilarité.<br>
'''ἱλάσκομαι (verbe)''' : apaiser, calmer.<br>
'''ἴλεξ, -κος (nom commun) (f)''' : houx.<br>
'''ἰλιγγιωδέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἰλιγγιώδης''.<br>
'''ἰλιγγιωδέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''ἰλιγγιώδης''.<br>
'''ἰλιγγιώδης, -ης, -ες (adjectif)''' : vertigineux.<br>
'''ἰλιγγιωδῶς (adverbe)''' : vertigineusement.<br>
'''ἰλιγγιωδώτατα, -, - (adverbe)''' : Superlatif de ''ἰλιγγιωδῶς''.<br>
'''ἰλιγγιωδώτερον, -, - (adverbe)''' : Comparatif de ''ἰλιγγιωδῶς''.<br>
'''ἴλιγγος, -ίγγου (nom commun) (m)''' : vertige.<br>
'''ἰλλυρικός, -ή, -όν (adjectif)''' : illyrien.<br>
'''ἰλλυρικῶς (adverbe)''' : illyriennement.<br>
'''ἰλλυρικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἰλλυρικός''.<br>
'''ἰλλυρικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἰλλυρικός''.<br>
'''ἰλλυρικώτατα, -, - (adverbe)''' : Superlatif de ''ἰλλυρικῶς''.<br>
'''ἰλλυρικώτερον, -, - (adverbe)''' : Comparatif de ''ἰλλυρικῶς''.<br>
'''ἴλλω (verbe)''' : avoir le vertige.<br>
'''ἰλύς, -ος (nom commun) (f)''' : Boue, sédiment. Impureté.<br>
'''ἱμάς, -άντος (nom commun) (m)''' : courroie.<br>
'''ἱμάσσω (verbe)''' : fouetter, flageller ; frapper.<br>
'''ἱμάτιον, -ίου (nom commun) (n)''' : himation.<br>
'''ἵμερος, -έρου (nom commun) (n)''' : désir amoureux.<br>
'''ἱμῶ (verbe)''' : .<br>
'''ἰξευτής, -οῦ (nom commun) (m)''' : oiseleur.<br>
'''ἰξεύω (verbe)''' : oiseler.<br>
'''ἰξός, -οῦ (nom commun) (m)''' : Glu ; gui.<br>
'''ἰός, -οῦ (nom commun) (m)''' : Venin ; flèche.<br>
'''ἰουδαῖος, -ία, -ῖον (adjectif)''' : juif.<br>
'''ἴου (interjection)''' : beurk.<br>
'''ἰπνός, -οῦ (nom commun) (m)''' : four.<br>
'''ἱππάζομαι (verbe)''' : chevaucher.<br>
'''ἱππαλεκτρυών, -όνος (nom commun) (m/f)''' : hippalectryon.<br>
'''ἱππασία, -ας (nom commun) (f)''' : chevauchée.<br>
'''ἱππαστί (adverbe)''' : à cheval.<br>
'''ἱππεύς, -έως (nom commun) (m)''' : cavalier.<br>
'''ἱππευτής, -οῦ (nom commun) (m)''' : cavalier.<br>
'''ἱππεύω (verbe)''' : monter à cheval.<br>
'''ἱππικός, -ή, -όν (adjectif)''' : équin.<br>
'''ἱππικῶς (adverbe)''' : équinement.<br>
'''ἱππικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἱππικός''.<br>
'''ἱππικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἱππικός''.<br>
'''ἱππικώτατα, -, - (adverbe)''' : Superlatif de ''ἱππικῶς''.<br>
'''ἱππικώτερον, -, - (adverbe)''' : Comparatif de ''ἱππικῶς''.<br>
'''ἵππος, -ου (nom commun) (m/f)''' : cheval, jument.<br>
'''ἱππόκαμπος, -άμπου (nom commun) (m)''' : hippocampe.<br>
'''ἱπποσύνη, -ης (nom commun) (f)''' : Équitation. (Militaire) Cavalerie.<br>
'''ἱπποπόταμος, -άμου (nom commun) (m)''' : hippopotame.<br>
'''ἱππο- (préfixe)''' : relatif au cheval.<br>
'''ἱππότης, -ου (nom commun) (m)''' : cavalier.<br>
'''ἴορκος, -όρκος (nom commun) (m)''' : Forme de ''δορκάς''.<br>
'''ἰός, -οῦ (nom commun) (m)''' : Flèche ; venin.<br>
'''ἰοῦ (interjection)''' : hourra.<br>
'''ἱρεύς, -έως (nom commun) (m)''' : Forme ionienne de ''ἱερεύς''.<br>
'''ἵρηξ, -κος (nom commun) (m)''' : Forme ionienne de ''ἱέραξ''.<br>
'''ἶρις, ἴριδος (nom commun) (f)''' : (anatomie) iris (météorologie) arc-en-ciel.<br>
'''ἰσαίτατα, -, - (adverbe)''' : Superlatif de ''ἴσως''.<br>
'''ἰσαίτερον, -, - (adverbe)''' : Comparatif de ''ἴσως''.<br>
'''ἴσατις, -δος (nom commun) (f)''' : guède.<br>
'''ἰσημερία, -ας (nom commun) (f)''' : équinoxe.<br>
'''ἰσορροπία, -ας (nom commun) (f)''' : équilibre.<br>
'''ϝίσϝος, -η, -ον (adjectif)''' : Forme arcado-chypriote et crétoise de ''ἴσος''.<br>
'''ἰσθμός, -οῦ (nom commun) (m)''' : isthme.<br>
'''ἴσοξ, -κου (nom commun) (m)''' : brochet.<br>
'''ἶσος, -η, -ον (adjectif)''' : Forme homérique de ''ἴσος''.<br>
'''ἴσος, -η, -ον (adjectif)''' : égal.<br>
'''ἴς, -νός (nom commun) (f)''' : force, puissance ; muscle.<br>
'''ἵστημι (verbe)''' : Placer ; mettre debout.<br>
'''ἱστίον, -ου (nom commun) (n)''' : os de la hanche.<br>
'''ἱστόρημα, -ήματος (nom commun) (n)''' : narration.<br>
'''ἱστορία, -ας (nom commun) (f)''' : Enquête, examination ; observation, étude.<br>
'''ἱστορικός, -ή, -όν (adjectif)''' : D’enquête, bien informé.<br>
'''ἱστορῶ (verbe)''' : enquêter.<br>
'''ἱστός, -οῦ (nom commun) (m)''' : mât.<br>
'''ἵστωρ, -ορος (nom commun) (m/f)''' : Individu sachant. Personne connaissant la loi. Juge.<br>
'''ἰσχίον, -ου (nom commun) (n)''' : os de la hanche.<br>
'''ἰσχνός, -ή, -όν (adjectif)''' : Desséché, sec. Maigre, grêle ; frêle.<br>
'''ἰσχνότατα, -, - (adverbe)''' : Superlatif de ''ἰσχνῶς''.<br>
'''ἰσχνότερον, -, - (adverbe)''' : Comparatif de ''ἰσχνῶς''.<br>
'''ἰσχνότατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ἰσχνός''.<br>
'''ἰσχνότερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ἰσχνός''.<br>
'''ἰσχνῶς (adverbe)''' : maigrement.<br>
'''ἴσχω (verbe)''' : tenir.<br>
'''ἴσως (adverbe)''' : également.<br>
'''ἰσαίτατος, -άτη, -ίτατον (adjectif)''' : Superlatif de ''ἴσος''.<br>
'''ἰσαίτερος, -έρα, -ίτερον (adjectif)''' : Comparatif de ''ἴσος''.<br>
'''ἰταλός, -οῦ (nom commun) (m)''' : Bouvillon, taurillon.<br>
'''ἰταμός, -ή, -όν (adjectif)''' : effronté.<br>
'''ἰταμότατα, -, - (adverbe)''' : Superlatif de ''ἰταμῶς''.<br>
'''ἰταμότερον, -, - (adverbe)''' : Comparatif de ''ἰταμῶς''.<br>
'''ἰταμότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ἰταμός''.<br>
'''ἰταμότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ἰταμός''.<br>
'''ἰταμότης, -ητος (nom commun) (f)''' : effronterie.<br>
'''ἰταμῶς (adverbe)''' : effrontément.<br>
'''-ίτης, -ου (suffixe) (m)''' : Suffixe nominal masculin.<br>
'''-ῖτις, -ίτιδος (suffixe) (f)''' : Suffixe nominal féminin.<br>
'''ἴυγξ, -γγος (nom commun) (f)''' : torcol.<br>
'''ἰύζω (verbe)''' : crier de peine ou de joie.<br>
'''ἴφυον, -ου (nom commun) (n)''' : .<br>
'''ἰχθυοέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ἰχθυόεις''.<br>
'''ἰχθυοέστερος, -έρα, -ερον (adjectif)''' : Comparatif de ''ἰχθυόεις''.<br>
'''ἰχθυόεις, -εσσα, -εν (adjectif)''' : poissonneux.<br>
'''ἰχθύς, -ος (nom commun) (m)''' : poisson.<br>
'''ἴχνος, -ους (nom commun) (n)''' : trace de pas.<br>
'''ἰώϐηλος, -ήλου (nom commun) (m)''' : jubilé.<br>
'''ἰῶτα (nom commun) (n)''' : iota.<br>
'''ἰωτακισμός, -οῦ (nom commun) (m)''' : iotacisme.<br>
'''Ἰάκωϐος, -ώϐου (nom propre) (m)''' : Jacob, Jacques.<br>
'''Ἰαπετός, -οῦ (nom propre) (m)''' : Japet.<br>
'''Ἴασος, -άσου (nom propre) (f)''' : Iasos (ville).<br>
'''Ἰάσων, -ονος (nom propre) (m)''' : Jason. (chef des Argonautes)<br>
'''Ἴδας, -α (nom propre) (m)''' : Idas.<br>
'''Ἰδομενεύς, -έως (nom propre) (m)''' : Idoménée.<br>
'''Ἰάμϐλιχος, -ίχου (nom propre) (m)''' : Jamblique.<br>
'''Ἰαω (nom propre) (m)''' : Yahweh.<br>
'''Ἰϐηρία, -ας (nom propre) (m)''' : Ibérie.<br>
'''Ἴϐηρ, -ος (nom commun) (m)''' : Ibère.<br>
'''Ἰγνάτιος, -ίου (nom propre) (m)''' : Ignace.<br>
'''Ἰεριχώ (nom propre) (f)''' : Jéricho.<br>
'''Ἰερειχώ (nom propre) (f)''' : Forme alternative de ''Ἰεριχώ''.<br>
'''Ἱερειχώ (nom propre) (f)''' : Forme alternative de ''Ἰεριχώ''.<br>
'''Ἱεριχοῦς (nom propre) (f)''' : Forme alternative de ''Ἰεριχώ''.<br>
'''Ἱεριχώ, -οῦς (nom propre) (f)''' : Forme alternative de ''Ἰεριχώ''.<br>
'''Ἰέρνη, -ης (nom propre) (f)''' : Irlande.<br>
'''Ἱεροκλῆς, -έους (nom propre) (m)''' : Hiéroclès.<br>
'''Ἱεροσάλημα (nom propre) (f)''' : Forme alternative de ''Ἱερουσαλήμ''.<br>
'''Ἱεροσόλυμα (nom propre) (f/n)''' : Forme alternative de ''Ἱερουσαλήμ''.<br>
'''Ἱερουσαλήμ (nom propre) (f)''' : Jérusalem (ville).<br>
'''Ἰερουσαλήμ (nom propre) (f)''' : Forme alternative de ''Ἱερουσαλήμ''.<br>
'''Ἱερώνυμος, -ύμου (prénom) (m)''' : Jérôme.<br>
'''Ἰεσσαί (nom propre) (m)''' : Jessé.<br>
'''Ἰεσσαῖος, -ίου (nom propre) (m)''' : Forme alternative de ''Ἰεσσαί''.<br>
'''Ἰησαΐας, -ου (nom propre) (m)''' : Forme alternative de ''Ἠσαΐας''.<br>
'''Ἰησοῦς, -οῦ (nom propre) (m)''' : Jésus, Josué.<br>
'''Ἰησαΐας, -ου (nom propre) (m)''' : Forme alternative de ''Ἠσαΐας''.<br>
'''Ἰησοῦς, -οῦ (nom propre) (m)''' : Jésus, Josué.<br>
'''Ἰθάκα, -ης (nom propre) (f)''' : Forme dorienne de ''Ἰθάκη''.<br>
'''Ἰθάκη, -ης (nom propre) (f)''' : Ithaque.<br>
'''Ἴθακος, -άκου (nom commun) (m)''' : Ithacien.<br>
'''Ἰκάριος, -ίου (nom propre) (m)''' : Icarios.<br>
'''Ἴκαρος, -άρου (nom propre) (m)''' : Icare.<br>
'''Ἴκελος, -έλου (nom propre) (m)''' : Icélos.<br>
'''Ἰλιάς, -δος (nom propre) (f)''' : Iliade.<br>
'''Ἴλιον, -ίου (nom propre) (n)''' : Ilion.<br>
'''Ἰλιάς, -δος (nom propre) (f)''' : Iliade.<br>
'''Ἴλιον, -ίου (nom propre) (n)''' : Ilion, Troie.<br>
'''Ἰλλυρία, -ας (nom propre) (f)''' : Illyrie.<br>
'''Ἰλλυρίς, -δος (nom commun) (f)''' : Illyrienne.<br>
'''Ἰλλυριός, -οῦ (nom commun) (m)''' : Illyrien.<br>
'''Ἶλος, Ἴλου (nom propre) (m)''' : Ilos (fils de Tros et de Callirrhoé).<br>
'''Ἵμερος, -έρου (nom propre) (m)''' : Himéros.<br>
'''Ἰμούθης, -ου (nom propre) (m)''' : Imhotep.<br>
'''Ἰξίων, -ονος (nom propre) (m)''' : Ixion.<br>
'''Ἰόλαος, -άου (nom propre) (m)''' : Iolaos.<br>
'''Ἰόλεως, -ώ (nom propre) (m)''' : Forme de ''Ἰόλαος''.<br>
'''Ἰουδαῖα, -ίας (nom propre) (f)''' : Judée.<br>
'''Ἰουδαῖα, -ίας (nom commun) (f)''' : Juive.<br>
'''Ἰουδαῖος, -ίου (nom commun) (m)''' : Juif.<br>
'''Ἰούδας, -α (nom propre) (m)''' : Judas.<br>
'''Ἰουδίθ (nom propre) (f)''' : Judith.<br>
'''Ἵππαρχος, -άρχου (nom commun) (m)''' : Hipparque (Fils cadet de Pisistrate, il fut assassiné par Harmodios et Aristogiton en −514 av. J.-C.).<br>
'''Ἶρις, Ἴριδος (nom propre) (f)''' : [[wikt:Iris|Iris]].<br>
'''Ἰσαάκ (nom propre) (m)''' : Issac.<br>
'''Ἴσακος, - (nom propre) (m)''' : Forme alternative de ''Ἰσαάκ''.<br>
'''Ἴσαυρα, -ας (nom propre) (f)''' : Isaura.<br>
'''Ἰσαυρία, -ας (nom propre) (f)''' : Isaurie.<br>
'''Ἴσαχος, - (nom propre) (m)''' : Forme alternative de ''Ἰσαάκ''.<br>
'''Ἰσίδωρος, -ώρου (nom propre) (m)''' : Isidore.<br>
'''Ἶσις, Ἴσιδος (nom propre) (f)''' : [[wikt:Isis|Isis]].<br>
'''Ἰσθμός, -οῦ (nom propre) (m)''' : Isthmos.<br>
'''Ἱσπανία, -ας (nom propre) (f)''' : Espagne.<br>
'''Ἱσπανίς, -δος (nom commun) (f)''' : Espagnole.<br>
'''Ἱσπανός, -οῦ (nom commun) (m)''' : Espagnol.<br>
'''Ἰσμαήλ (nom propre) (m)''' : Ismaël.<br>
'''Ἰσραήλ (nom propre) (m)''' : Israël.<br>
'''Ἴστρος, -ου (nom propre) (f)''' : Danube inférieur.<br>
'''Ἰταλία -ας (nom propre) (f)''' : Italie.<br>
'''Ἰταλίς, -δος (nom commun) (f)''' : Italienne.<br>
'''Ἰταλιώτης, -ου (nom commun) (m)''' : Italiote.<br>
'''Ἱταλιῶτις, -ώτιδος (nom commun) (f)''' : Italiote.<br>
'''Ἰταλός, -οῦ (nom commun) (m)''' : Italien.<br>
'''Ἴτυς, -ος (nom propre) (m)''' : Itys.<br>
'''Ἴϋγξ, -γγος (nom propre) (f)''' : [[wikt:Jynx|Jynx]].<br>
'''Ἰφθίμη, -ης (nom propre) (f)''' : Iphtimé (sœur de Pénélope).<br>
'''Ἰφιάνασσα, -ας (nom propre) (f)''' : Forme homérique de ''Ἰφιγένεια''.<br>
'''Ἰφιγένεια, -ίας (nom propre) (f)''' : Iphigénie.<br>
'''Ἰφικλῆς, -έους (nom propre) (m)''' : Iphiclès (demi-frère jumeau d'Héraclès).<br>
'''Ἰχθύες, -ων (nom propre) (m)''' : Poissons.<br>
'''Ἰωάννα, -ας (prénom) (f)''' : Jeanne.<br>
'''Ἰωάννης ὁ Χρυσόστομος (nom propre) (m)''' : Jean Chrysostome.<br>
'''Ἰωήλ (nom propre) (m)''' : Joël.<br>
'''Ἰωνάθαν (nom propre) (m)''' : Jonathan.<br>
'''Ἰωνᾶς, -ᾶ (nom propre) (m)''' : Jonas.<br>
'''Ἰωνία, -ας (nom propre) (f)''' : Ionie.<br>
'''Ἴων, -ος (nom propre) (m)''' : Ion (fils de Xouthos).<br>
'''Ἴων, -ος (nom commun) (m)''' : Ionien.<br>
'''Ἰωσήφ (nom propre) (m)''' : Joseph.<br>
'''Ἰωσίας, -ου (nom propre) (m)''' : Josias.<br>
'''Ἰώ, -οῦς (nom propre) (f)''' : [[wikt:Io|Io]].<br>
==Κ==
'''ϗ (symbole)''' : Et. (Abréviation graphique valant ''καί'' en grec comme ''&'' valant ''et'' en français.)<br>
'''καδεμών, -όνος (nom commun) (m)''' : Forme dorienne de ''κηδεμών''.<br>
'''καδιμών, -όνος (nom commun) (m)''' : Forme éolienne de ''κηδεμών''.<br>
'''κάδος, -ου (nom commun) (m)''' : seau.<br>
'''καθαίρω (verbe)''' : Purger, nettoyer ; laver. Étriller.<br>
'''καθαρεύω (verbe)''' : purifier.<br>
'''καθαρίζω (verbe)''' : Nettoyer, purifier. (Médecine) Guérir d’une infection.<br>
'''καθαρός, -ά, -όν (adjectif)''' : pur.<br>
'''καθαρῶς (adverbe)''' : purement.<br>
'''καθαρώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''καθαρός''.<br>
'''καθαρώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''καθαρός''.<br>
'''καθέδρα, ας (nom commun) (f)''' : Ce qui sert à s'asseoir. Action d'être ou de demeurer assis.<br>
'''καθετήρ, -ῆρος (nom commun) (m)''' : Ligne pour pêcher. Collier, pendant ; ornement de femme.<br>
'''καθεύδω (verbe)''' : dormir.<br>
'''καθηγέομαι (verbe)''' : professer.<br>
'''καθηγήτειρα, -ίρας (nom commun) (f)''' : professeure.<br>
'''καθηγητής, -οῦ (nom commun) (m)''' : professeur.<br>
'''καθηγητικός, -ή , -όν (adjectif)''' : professoral.<br>
'''καθηγητικῶς (adverbe)''' : professoralement.<br>
'''καθηγητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''καθηγητικός''.<br>
'''καθηγητικώτερος, -έρη, -ώτερον (adjectif)''' : Comparatif de ''καθηγητικός''.<br>
'''καθηγητικώτατα, -, - (adverbe)''' : Superlatif de ''καθηγητικῶς''.<br>
'''καθηγητικώτερον, -, - (adverbe)''' : Comparatif de ''καθηγητικῶς''.<br>
'''καθήλωσις, -ώσεως (nom commun) (f)''' : détention.<br>
'''καθηλῶ (verbe)''' : détenir.<br>
'''καθίημι (verbe)''' : descendre.<br>
'''κάθημαι (verbe)''' : siéger.<br>
'''καθολικός, -ή, -όν (adjectif)''' : Général ; universel.<br>
'''καθολικῶς (adverbe)''' : Généralement ; universellement.<br>
'''καθολικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''καθολικός''.<br>
'''καθολικώτερος, -έρη, -ώτερον (adjectif)''' : Comparatif de ''καθολικός''.<br>
'''καθολικώτατα, -, - (adverbe)''' : Superlatif de ''καθολικῶς''.<br>
'''καθολικώτερον, -, - (adverbe)''' : Comparatif de ''καθολικῶς''.<br>
'''καθοράω (verbe)''' : regarder.<br>
'''καί (conjonction)''' : Et ; Et même, même. Et en outre. Et ensuite, puis. Ou.<br>
'''καινός, -ή, -όν (adjectif)''' : nouveau.<br>
'''καινότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''καινός''.<br>
'''καινότερος, -έρη, -ότερον (adjectif)''' : Comparatif de ''καινός''.<br>
'''καινότατα, -, - (adverbe)''' : Superlatif de ''καινῶς''.<br>
'''καινότερον, -, - (adverbe)''' : Comparatif de ''καινῶς''.<br>
'''καίνωσις, -ώσεως (nom commun) (f)''' : renouvellement.<br>
'''καινῶς (adverbe)''' : nouvellement.<br>
'''καῖρος, -ίρου (nom commun) (m)''' : embrouille.<br>
'''καιρός, -οῦ (nom commun) (m)''' : temps (durée, époque).<br>
'''καιρόω (verbe)''' : embrouiller.<br>
'''καίρωσις, -ώσεως (nom commun) (f)''' : embrouillage.<br>
'''καίω (verbe)''' : brûler.<br>
'''κάκιον, -, - (adverbe)''' : Comparatif de ''κακῶς''.<br>
'''κάκιστα, -, - (adverbe)''' Superlatif de ''κακῶς''.<br>
'''κακοδαίμων, -ων, -ον (adjectif)''' : malheureux.<br>
'''κακοδαιμόνως (adverbe)''' : malheureusement.<br>
'''κακοδαιμονέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''κακοδαίμων''.<br>
'''κακοδαιμονέστερος, -έρα, -έρερον (adjectif)''' : Comparatif de ''κακοδαίμων''.<br>
'''κακοήθης, -ης, -ες (adjectif)''' : malin.<br>
'''κακοῦργος, -ύργα, -ῦργον (adjectif)''' : Dérangé ; malfaisant.<br>
'''κακός, -ή, όν (adjectif)''' : Mauvais ; laid.<br>
'''κακο- (préfixe)''' : mauvais.<br>
'''κακοφωνία, -ας (nom commun) (f)''' : mauvaise sonorité.<br>
'''κακκῶ (verbe)''' : déféquer ; chier.<br>
'''κακῶς (adverbe)''' : mal.<br>
'''κάλαθος, -άθου (nom commun) (m)''' : corbeille, panier.<br>
'''καλάϊνος, -ΐνη, -άϊνον (adjectif)''' : bleu ciel.<br>
'''καλαΐνως (adverbe)''' : .<br>
'''καλαΐνωτατος, -άτη, -καλαΐνωτατον (adjectif)''' : Superlatif de ''καλάϊνος''.<br>
'''καλαϊνώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''καλάϊνος''.<br>
'''καλαϊνώτατος, -, - (adverbe)''' : Superlatif de ''καλαΐνως''.<br>
'''καλαΐνωτερον, -, - (adverbe)''' : Comparatif de ''καλαΐνως''.<br>
'''κάλιον, -, - (adverbe)''' : Comparatif de ''καλῶς''.<br>
'''κάλιστα, -, - (adverbe)''' Superlatif de ''καλῶς''.<br>
'''καλλι- (préfixe)''' : beau.<br>
'''καλλίπυγοσις, -όσεως (nom commun) (f)''' : .<br>
'''καλλίπυγος, -ος, -ον (adjectif)''' : Qui a de belles fesses.<br>
'''κάλλιστος, -, - (adjectif)''' : Superlatif de ''καλός''.<br>
'''καλλίων, -, - (adjectif)''' : Comparatif de ''καλός''.<br>
'''κάλλος, -ους (nom commun) (n)''' : beauté.<br>
'''καλοήθης, -ης, -ες (adjectif)''' : bénin.<br>
'''καλός, -ή, -όν (adjectif)''' : Beau ; bon.<br>
'''κάλος, -η, -ον (adjectif)''' : Forme éolienne de ''καλός''.<br>
'''καλϝός, -ή, -όν (adjectif)''' : Forme béotienne de ''καλός''.<br>
'''κάλυμμα, -ύμματος (nom commun) (n)''' : couverture.<br>
'''κάλυξ, -κος (nom commun) (m/f)''' : (Botanique) Calice des fleurs ; écale ou peau des fruits.<br>
'''καλύπτω (verbe)''' : Couvrir ; envelopper, cacher.<br>
'''καλῶς (adverbe)''' : Bien ; d’une belle manière.<br>
'''καλῶ (verbe)''' : appeler.<br>
'''καμάρα, -ας (nom commun) (f)''' : Voûte ; chambre voûtée.<br>
'''καμάρη, -ης (nom commun) (f)''' : Forme ionienne de ''καμάρα''.<br>
'''κάμαξ, -κος (nom commun) (m/f)''' : Perche. Hampe de la lance. (Marine) Gouvernail, manche de la rame.<br>
'''κάμηλος, -ήλου (nom commun) (m/f)''' : chameau.<br>
'''καμηλοπάρδαλις, -άλεως (nom commun) (f)''' : girafe.<br>
'''κάμινος, -ίνου (nom commun) (m)''' : cheminée.<br>
'''κάμπη, -ης (nom commun) (f)''' : chenille.<br>
'''καμπή, -ῆς (nom commun) (f)''' : articulation.<br>
'''κάμπος, -ους (nom commun) (n)''' : poisson marin.<br>
'''κάπρος, -ου (nom commun) (n)''' : sanglier.<br>
'''κάμπτω (verbe)''' : courber.<br>
'''καμπύλη, -ης (nom commun) (f)''' : courbure.<br>
'''καμπύλος, -η, -ον (adjectif)''' : Courbé, tordu. Voilé (en parlant d’une roue.)<br>
'''καμπυλότης, -τος (nom commun) (f)''' : courbure.<br>
'''κανδάκη, -ης (nom commun) (f)''' : candace.<br>
'''κανάσσω (verbe)''' : cliquer.<br>
'''καναχή, -ῆς (nom commun) (f)''' : clic.<br>
'''καναχίζω (verbe)''' : claquer.<br>
'''καναχηδά (adverbe)''' : .<br>
'''καναχής, -ής, -ές (adjectif)''' : .<br>
'''καναχῶ (verbe)''' : cliquer.<br>
'''κἄν (conjonction)''' : .<br>
'''κάνθαρος, -άρου (nom commun) (m)''' : cafard.<br>
'''κανονικός, -ή, -όν (adjectif)''' : régulier.<br>
'''κανονικῶς (adverbe)''' : régulièrement.<br>
'''κανονικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κανονικός''.<br>
'''κανονικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κανονικός''.<br>
'''κανονικώτατα, -, - (adverbe)''' : Superlatif de ''κανονικῶς''.<br>
'''κανονικώτερον, -, - (adverbe)''' : Comparatif de ''κανονικῶς''.<br>
'''κανών, -όνος (nom commun) (m)''' : Tige de roseau, règle de maçon ou de charpentier. (Figuré) Règle, modèle ; principe.<br>
'''κάνωπον, -ώπου (nom commun) (n)''' : chou-fleur.<br>
'''καπηλεῖον, -ίου (nom commun) (n)''' : taverne.<br>
'''καπηλεύω (verbe)''' : tenir une taverne.<br>
'''καπηλικός, -ή, -όν (adjectif)''' : tavernier.<br>
'''καπηλίς, -δος (nom commun) (f)''' : tavernière.<br>
'''κάπηλος, -ήλου (nom commun) (m)''' : tavernier.<br>
'''κάππα (nom commun) (n)''' : kappa.<br>
'''κάπτω (verbe)''' : Avaler, prendre (une bouffée d’air).<br>
'''κάρα, -τος (nom commun) (n)''' : Tête ; visage.<br>
'''κάραϐος, -άϐου (nom commun) (m)''' : Crabe ; langouste. Scarabée ; escarbot.<br>
'''κάρδαμον, -ου (nom commun) (n)''' : cresson.<br>
'''καρδία, -ας (nom commun) (f)''' : cœur.<br>
'''καρδιακός, -ή, -ός (adjectif)''' : cardiaque.<br>
'''καρδιακῶς (adverbe)''' : cardiaquement.<br>
'''καρδιακώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''καρδιακός''.<br>
'''καρδιακώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''καρδιακός''.<br>
'''καρδούχιος, -ος, -ον (adjectif)''' : kurde.<br>
'''καρηϐάρεια, -ίας (nom commun) (f)''' : mal de tête.<br>
'''κάρη, -τος (nom commun) (n)''' : Forme homérique et ionienne de ''κάρα''.<br>
'''καρίς, -δος (nom commun) (f)''' : crevette.<br>
'''καρκίνος, -ίνου (nom commun) (m)''' : (Zoologie) Crabe. (Astronomie) Constellation du Cancer. (Médecine) Chancre, tumeur, cancer.
Pince pour saisir ou tenir les objets dans le feu. Compas. Sorte de bandage. (Habillement) Sorte de chaussure.<br>
'''καρπάλιμος, -ος, -ον (adjectif)''' : rapide.<br>
'''καρπός, -οῦ (nom commun) (m)''' : (Sens propre) Fruit, graine ; produit, récolte. (Sens figuré) Produit de quelque chose : enfant (produit du corps), poésie (produit de l'esprit), profit. (Anatomie) Poignet.<br>
'''καρύκιον, -ίου (nom commun) (m)''' : Forme dorienne de ''κηρύκειον''.<br>
'''κᾶρυξ, -άρυκος (nom commun) (m)''' : Forme dorienne de ''κῆρυξ''.<br>
'''καρυόφυλλον, -ύλλου (nom commun) (n)''' : œillet.<br>
'''καρύσσω (verbe)''' : Forme dorienne de ''κηρύσσω''.<br>
'''κάρφος, -ους (nom commun) (n)''' : brindille.<br>
'''κάρχαρος, -α, -ον (adjectif)''' : aiguisé.<br>
'''καρχηδονιακός, -ή, -όν (adjectif)''' : carthaginois.<br>
'''καρῶτον, -ώτου (nom commun) (n)''' : carotte.<br>
'''κάστανον, -άνου (nom commun) (n)''' : châtaigne.<br>
'''κάστον -ου (nom commun) (n)''' : bois.<br>
'''κατά (adverbe ; préposition)''' (Devient ''κατ᾽'' devant un mot commençant par une voyelle à esprit doux, et ''καθ᾽'' devant un mot commençant par une voyelle à esprit rude.) : De haut en bas. En bas. En dessous, au fond. (Par extension) À fond, complètement.
(Avec le génitif) marque l’origine, le point de départ ou le point d’arrivée. Contre, idée d’hostilité. (Avec l’accusatif) suivant, selon.
Par, avec l'idée de succession (un par un). Pendant, avec l’idée de temps. Après, avec l’idée de succession temporelle (jour après jour). À travers, idée de transpercement, de part en part, d’un bout à l’autre.<br>
'''καταϐιϐρώσκω (verbe)''' : .<br>
'''καταγέλαστος, -ος, -ον (adjectif)''' : ridicule.<br>
'''καταγλώττισμα, -ίσματος (nom commun) (n)''' : baiser intrabuccal.<br>
'''καταγράφω (verbe)''' : condamner.<br>
'''καταδικάζω (verbe)''' : condamner.<br>
'''καταδίκη, -ης (nom commun) (f)''' : condamnation.<br>
'''καταδικαστικός, -ή, -όν (adjectif)''' : condamnable.<br>
'''κατάδικος, -ίκου (nom commun) (m/f)''' : condamné.<br>
'''κατάδυσις, -ύσεως (nom commun) (f)''' : émergence.<br>
'''καταδύομαι (verbe)''' : plonger.<br>
'''κατακλύζω (verbe)''' : inonder.<br>
'''κατάκλυσις, -ύσεως (nom commun) (f)''' : inondation.<br>
'''κατακλυσμιαίος, -α, -ον (adjectif)''' : diluvien.<br>
'''κατακλυσμικός, -ή, -όν (adjectif)''' : diluvien.<br>
'''κατακλυσμικότατα, -, - (adverbe)''' : Superlatif de ''κατακλυσμικῶς''.<br>
'''κατακλυσμικότερον, -, - (adverbe)''' : Comparatif de ''κατακλυσμικῶς''.<br>
'''κατακλυσμικῶς (adverbe)''' : désastreusement.<br>
'''κατακλυσμικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κατακλυσμικός''.<br>
'''κατακλυσμικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κατακλυσμικός''.<br>
'''κατακλυσμός, -οῦ (nom commun) (m)''' : déluge.<br>
'''κατακτητής, -οῦ (nom commun) (m)''' : conquérant.<br>
'''κατακτῶμαι (verbe)''' : conquérir.<br>
'''καταλαμϐάνω (verbe)''' : saisir.<br>
'''κατάλαψις, -άψεως (nom commun) (f)''' : Forme dorienne de ''κατάληψις''.<br>
'''κατάληψις, -ήψεως (nom commun) (f)''' : Saisissement. Prise de possession, occupation.<br>
'''κατάλογος, -όγου (nom commun) (m)''' : liste.<br>
'''καταλογογράφησις, -ήσεως (nom commun) (f)''' : listage.<br>
'''καταλογογραφῶ (verbe)''' : lister.<br>
'''κατάπλασμα, -άσματος (nom commun) (n)''' : cataplasme.<br>
'''κατάπτωσις, -ώσεως (nom commun) (f)''' : décadence.<br>
'''κατάπυγον, -ύγου (nom commun) (n)''' : doigt d'honneur.<br>
'''κατάρα, -ας (nom commun) (f)''' : malédiction.<br>
'''καταριέμαι (verbe)''' : maudire.<br>
'''καταστροφικός, -ή, -όν (adjectif)''' : désastreux.<br>
'''καταστροφικότατα, -, - (adverbe)''' : Superlatif de ''καταστροφικῶς''.<br>
'''καταστροφικότερον, -, - (adverbe)''' : Comparatif de ''καταστροφικῶς''.<br>
'''καταστροφικῶς (adverbe)''' : désastreusement.<br>
'''καταστροφικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''καταστροφικός''.<br>
'''καταστροφικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''καταστροφικός''.<br>
'''καταστροφή, -ῆς (nom commun) (f)''' : Désastre, destruction ; renversement.<br>
'''κατάσχεσις, -έσεως (nom commun) (f)''' : confiscation.<br>
'''κατάταξις, -άξεως (nom commun) (f)''' : mise en ordre.<br>
'''κατατάσσω (verbe)''' : mettre en ordre.<br>
'''καταφυγή, -ῆς (nom commun) (f)''' : .<br>
'''καταφύγιον, -ίου (nom commun) (n)''' : abri.<br>
'''κάτεργον, -έργου (nom commun) (n)''' : bagne.<br>
'''κατηγορῶ (verbe)''' : accuser.<br>
'''κατηγορία, -ας (nom commun) (f)''' : Charge ; accusation.<br>
'''κατηγορίη, -ης (nom commun) (f)''' : Forme ionienne de ''κατηγορία''.<br>
'''κατήχησις, -ήσεως (nom commun) (f)''' : .<br>
'''κατηχίζω (verbe)''' : .<br>
'''κατηχισμός, -οῦ (nom commun) (m)''' : .<br>
'''κατηχιστής, -οῦ (nom commun) (m)''' : .<br>
'''κατηχούμενος, -ένου (nom commun) (m)''' : .<br>
'''κατηχῶ (verbe)''' : .<br>
'''κάτοπτρον, -όπτρου (nom commun) (m)''' : miroir.<br>
'''κατώτατος, -άτη, -ώτατον (adjectif)''' : .<br>
'''κατώτερος, -έρα, -ώτερον (adjectif)''' : inférieur.<br>
'''κατωτερότης, -τος (nom commun) (f)''' : infériorité.<br>
'''κάτω (adverbe)''' : sous.<br>
'''καῦκος, -ύκου (nom commun) (m)''' : .<br>
'''καῦµα, -ύματος (nom commun) (n)''' : chaleur de l’été.<br>
'''καύσις, -εως (nom commun) (f)''' : combustion.<br>
'''καυστός, -ή, -όν (adjectif)''' : torride.<br>
'''καυστῶς (adverbe)''' : torridement.<br>
'''καυστώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''καυστός''.<br>
'''καυστώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''καυστός''.<br>
'''καυστώτατα, -, - (adverbe)''' : Superlatif de ''καυστῶς''.<br>
'''καυστώτερον, -, - (adverbe)''' : Comparatif de ''καυστῶς''.<br>
'''καύχημα, -ήματος (nom commun) (n)''' : vantardise.<br>
'''καυχός, -οῦ (nom commun) (m)''' : Forme crétoise de ''χαλκός''.<br>
'''καυχῶμαι (verbe)''' : .<br>
'''κεγχριαῖος, -ία, -ῖον (adjectif)''' : granuleux.<br>
'''κέγχρος, -ου (nom commun) (m)''' : (Botanique) Millet. Grain.<br>
'''κεῖμαι (verbe)''' : être couché, se reposer ; être situé.<br>
'''κείμενον, -ένου (nom commun) (n)''' : texte.<br>
'''κεινός, -ή, -όν (adjectif)''' : Forme ionienne de ''κενός''.<br>
'''κείρω (verbe)''' : .<br>
'''κελαινός, -ή, -όν (adjectif)''' : obscur.<br>
'''κελτικός, -ή, -όν (adjectif)''' : celte.<br>
'''κελτικῶς (adverbe)''' : .<br>
'''κελτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κελτικός''.<br>
'''κελτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κελτικός''.<br>
'''κελτιστί (adverbe)''' : en grec.<br>
'''κενός, -ή, -όν (adjectif)''' : Vide. Vain, frivole.<br>
'''κενϝός, -ή, -όν (adjectif)''' : Forme ancienne de ''κενός''.<br>
'''κενεός, -ή, -όν (adjectif)''' : Forme poétique de ''κενός''.<br>
'''κεντρικός, -ή, -όν (adjectif)''' : central.<br>
'''κέντρον, -ου (nom commun) (n)''' : Aiguillon, dard. Pointe du compas, d’où centre du cercle.<br>
'''κεντῶ (verbe)''' : piquer.<br>
'''κενῶς (adverbe)''' : Habilement, sagement. Ingénieusement, finement.<br>
'''κενώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κενός''.<br>
'''κενώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κενός''.<br>
'''κεραμεύς, -έως (nom commun) (m)''' : potier.<br>
'''κεραμικός, -ή, -όν (adjectif)''' : d’argile.<br>
'''κέραμος, -άμου (nom commun) (m)''' : argile.<br>
'''κεράννυμι (verbe)''' : mélanger.<br>
'''κέρας, -τος (nom commun) (n)''' : Corne. (Par analogie) Bras d’un fleuve ; aile d’une armée ou d’une flotte ; antenne ou vergue d’un navire ; pic d’une montagne. (Sophisme) argument cornu.<br>
'''κεραός, -ά, -όν (adjectif)''' : Cornu ; fait de corne.<br>
'''κεραϝός, -ά, -όν (adjectif)''' : Forme ancienne de ''κεραός''.<br>
'''κερασός, -οῦ (nom commun) (m)''' : cerise.<br>
'''κεραυνός, -οῦ (nom commun) (m)''' : foudre.<br>
'''κέρκηρις, -ήρεως (nom commun) (m)''' : sarcelle.<br>
'''κερκοπίθακος, -άκου (nom commun) (m)''' : Forme dorienne de ''κερκοπίθηκος''.<br>
'''κερκοπίθηκος, -ήκου (nom commun) (m)''' : cercopithèque.<br>
'''κέρκος, -ου (nom commun) (f)''' : queue.<br>
'''κέρκωψ, -πος (nom commun) (m)''' : singe à longue queue.<br>
'''κεστός, -ή, -όν (adjectif)''' : brodé.<br>
'''κεστότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''κεστός''.<br>
'''κεστότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''κεστός''.<br>
'''κεστῶς (adverbe)''' : .<br>
'''κεφαλαία, -ας (nom commun) (f)''' : mal de tête.<br>
'''κεφαλή, -ῆς (nom commun) (f)''' : tête.<br>
'''κεφάλιον, -ίου (nom commun) (n)''' : petite tête.<br>
'''κεφαλίς, -δος (nom commun) (f)''' : bonnet, chapeau.<br>
'''κηδεμών, -όνος (nom commun) (m)''' : .<br>
'''κηκίς, -ῖδος (nom commun) (f)''' : Jet. Teinture tiré de la galle.<br>
'''κηλίς, -ῖδος (nom commun) (f)''' : .<br>
'''κνημίς, -ῖδος (nom commun) (f)''' : .<br>
'''κρηπίς, -ῖδος (nom commun) (f)''' : .<br>
'''κῆπος, -ήπου (nom commun) (m)''' : jardin ; sorte de singe.<br>
'''κηρός, -οῦ (nom commun) (m)''' : cire.<br>
'''κῆρ, -ος (nom commun) (n)''' : cœur.<br>
'''κῆρυγμα, -ύγματος (nom commun) (n)''' : proclamation à voix haute.<br>
'''κηρύκειον, -ίου (nom commun) (m)''' : caducée.<br>
'''κῆρυξ, -ήρυκος (nom commun) (m)''' : héraut.<br>
'''κηρύσσω (verbe)''' : annoncer.<br>
'''κηρύττω (verbe)''' : Forme attique de ''κηρύσσω''.<br>
'''κηφήν, -ῆνος (nom commun) (m)''' : bourdon.<br>
'''κηφηνώδης, -ης, -ες (adjectif)''' : .<br>
'''κιϐδηλεύω (verbe)''' : falsifier.<br>
'''κιϐδηλία, -ας (nom commun) (f)''' : falsification.<br>
'''κίϐδηλος, -ος, -ου (adjectif)''' : falsifié ; frauduleux.<br>
'''κιϐδηλότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''κίϐδηλος''.<br>
'''κιϐδηλότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''κίϐδηλος''.<br>
'''κιϐδήλως (adverbe)''' : frauduleusement.<br>
'''κιϐωτός, -οῦ (nom commun) (f)''' : arche.<br>
'''κιγκλίς, -δος (nom commun) (f)''' : bergeronnette.<br>
'''κιδαφεύω (verbe)''' : ruser.<br>
'''κίδαφος, -άφη, -ίδαφον (adjectif)''' : rusé.<br>
'''κίδαφότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''κίδαφος''.<br>
'''κίδαφότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''κίδαφος''.<br>
'''κιδάφως (adverbe)''' : frauduleusement.<br>
'''κικκάϐη, -ης (nom commun) (f)''' : .<br>
'''κῖκυς, -ίκυος (nom commun) (f)''' : force ; vigueur.<br>
'''κιθάρα, -ας (nom commun) (f)''' : cithare.<br>
'''κιθάρη, -ης (nom commun) (f)''' : Forme ionienne de ''κιθάρα''.<br>
'''κιθών, -ῶνος (nom commun) (m)''' : Forme ionienne de ''χιτών''.<br>
'''κίνδυνος, -ύνου (nom commun) (m)''' : Danger, risque ; entreprise hasardeuse.<br>
'''κινηθμός, -οῦ (nom commun) (m)''' : .<br>
'''κίνημα, -ήματος (nom commun) (n)''' : mouvement (chose).<br>
'''κίνησις, -ήσεως (nom commun) (f)''' : mouvement (processus).<br>
'''κινητέον, -ου (nom commun) (n)''' : .<br>
'''κινητήρ, -ῆρος (nom commun) (m)''' : .<br>
'''κινητής, -οῦ (nom commun) (m)''' : .<br>
'''κινητήριος, -α, -ον (adjectif)''' : .<br>
'''κινητικός, -ή, -όν (adjectif)''' : .<br>
'''κινητός, -ή, -όν (adjectif)''' : .<br>
'''κίνητρον, -ήτρου (nom commun) (n)''' : .<br>
'''κίνναμον, -άμου (nom commun) (n)''' : cannelle.<br>
'''κινῶ (verbe)''' : bouger.<br>
'''κιξάλλης, -ου (nom commun) (m)''' : voleur de grand chemin.<br>
'''κίρκος, -ου (nom commun) (m)''' : (ornithologie) Faucon. Anneau.<br>
'''κιρρός, -ά, -όν (adjectif)''' : brun-roux.<br>
'''κισσαϐίζω (verbe)''' : .<br>
'''κῖσσα, -ης (nom commun) (f)''' : geai des chênes.<br>
'''κίσσα, -ης (nom commun) (f)''' : pie.<br>
'''κισσός, -οῦ (nom commun) (m)''' : lierre.<br>
'''κισσοστεφής, -ής, -ές (adjectif)''' : couronné de lierre.<br>
'''κισσῶ (verbe)''' : .<br>
'''κίττα, -ης (nom commun) (f)''' : Forme attique de ''κίσσα''.<br>
'''κιτών, -ῶνος (nom commun) (m)''' : Forme dorienne de ''χιτών''.<br>
'''κιχόρη, -ης (nom commun) (f)''' : chicorée.<br>
'''κλάδος, -ου (nom commun) (m)''' : Branche (d'arbre), rameau.<br>
'''κλαΐς, -δός (nom commun) (f)''' : Forme dorienne de ''κλείς''.<br>
'''κλᾶϊς, -άϊδος (nom commun) (f)''' : Forme éolienne de ''κλείς''.<br>
'''κλαίω (verbe)''' : Pleurer sur, déplorer. Appeler en criant. Se mettre en pleurs.<br>
'''κλᾶμμα, -άμματος (nom commun) (f)''' : Forme éolienne de ''κλῆμα''.<br>
'''κλαστός, -ή, -όν (adjectif)''' : brisé, cassé.<br>
'''κλαυθμυρίζω (verbe)''' : faire pleurer.<br>
'''κλαῦμα, -ύματος (nom commun) (n)''' : pleur.<br>
'''κλαυσίγελως, -τος (nom commun) (m)''' : .<br>
'''κλαυστός, -ή, -όν (adjectif)''' : Pleuré. Funèbre, triste.<br>
'''κλάω (verbe)''' : Briser, casser.<br>
'''κλεῖθρον, -ίθρου (nom commun) (n)''' : serrure.<br>
'''κλειθροποιός, -οῦ (nom commun) (m)''' : serrurier.<br>
'''κλείς, -δός (nom commun) (f)''' : clé.<br>
'''κλειτορίς, -δος (nom commun) (f)''' : clitoris.<br>
'''κλείω (verbe)''' : fermer.<br>
'''κλέπτης, -ου (nom commun) (m)''' : voleur.<br>
'''κλέπτω (verbe)''' : voler. (dérober)<br>
'''κληΐς, -δός (nom commun) (f)''' : Forme ionienne de ''κλείς''.<br>
'''κλῄς, -δός (nom commun) (f)''' : Forme attique de ''κλείς''.<br>
'''κλέος, -ους (nom commun) (n)''' : gloire.<br>
'''κλεψύδρα, -ας (nom commun) (m)''' : clepsydre.<br>
'''κλέω (verbe)''' : être célèbre.<br>
'''κλῆμα, -ήματος (nom commun) (n)''' : pampre.<br>
'''κληματίζω (verbe)''' : .<br>
'''κληματικός, -ή, -όν (adjectif)''' : .<br>
'''κλημάτινος, -η, ον (adjectif)''' : .<br>
'''κληματίς, -δος (nom commun) (f)''' : Diminutif de ''κλῆμα''.<br>
'''κληματῖτις, -ίτιδος (nom commun) (f)''' : .<br>
'''κληματόδεσις, -εως (nom commun) (f)''' : .<br>
'''κληματοειδής, -ής, -ές (adjectif)''' : .<br>
'''κληματόεις, -εσσα, εν (adjectif)''' : .<br>
'''κληματόομαι (verbe)''' : .<br>
'''κληματώδης, -ης, -ες (adjectif)''' : .<br>
'''κλήρωσις, -ώσεως (nom commun) (f)''' : .<br>
'''κληρονομία, -ας (nom commun) (f)''' : héritage.<br>
'''κληρονομικότης, -τος (nom commun) (f)''' : hérédité.<br>
'''κληρονομικός, -ή, -όν (adjectif)''' : héréditaire.<br>
'''κληρονόμος, -ου (nom commun) (m)''' : héritier.<br>
'''κληρονομῶ (verbe)''' : hériter.<br>
'''κλῆρος, -ήρου (nom commun) (m)''' : .<br>
'''κληρόω (verbe)''' : .<br>
'''κλητήρ, -ῆρος (nom commun) (m)''' : Témoin ; héraut.<br>
'''κλίμα, -τος (nom commun) (n)''' : Inclinaison, pente. Inclinaison de la Terre, latitude, climat.<br>
'''κλῖμαξ, -ίμακος (nom commun) (f)''' : Escalier ; échelle.<br>
'''κλίνη, -ης (nom commun) (f)''' : lit.<br>
'''κλίνω (verbe)''' : Pencher. Pencher sur, s’appuyer sur. Coucher, allonger, étendre.<br>
'''κλίννω (verbe)''' : Forme éolienne de ''κλίνω''.<br>
'''κλίσις, -εως (nom commun) (f)''' : déclinaison.<br>
'''κλίτος, -ους (nom commun) (n)''' : allée.<br>
'''κλῖτος, -ίτους (nom commun) (n)''' : .<br>
'''κλιτύς, -ος (nom commun) (n)''' : Forme béotienne de ''κλῖτος''.<br>
'''κλόνις, -ος (nom commun) (f)''' : sacrum.<br>
'''κλόνος, -ου (nom commun) (m)''' : Confusion ; excitation.<br>
'''κλύδων, -ος (nom commun) (m)''' : Vague. Trouble, agitation, mouvement tumultueux.<br>
'''κλύζω (verbe)''' : Laver, nettoyer. Battre de ses flots, baigner de ses flots.<br>
'''κλυστήρ, -έρος (nom commun) (m)''' : seringue.<br>
'''κλύω (verbe)''' : entendre.<br>
'''κλωβός, -ϐοῦ (nom commun) (m)''' : cage à oiseau.<br>
'''κλών, -ός (nom commun) (m)''' : .<br>
'''κλώψ, -πός (nom commun) (m)''' : voleur.<br>
'''κλῶ (verbe)''' : S’entendre nommer ; entendre parler de soi.<br>
'''κνέφας, -ους (nom commun) (n)''' : Aube ; crépuscule, obscurité.<br>
'''κνῆκος, -ήκου (nom commun) (f)''' : carthame des teinturiers.<br>
'''κνήμη, -ης (nom commun) (f)''' : jambe.<br>
'''κνίδη, -ης (nom commun) (f)''' : ortie.<br>
'''κνίδωσις, -ώσεως (nom commun) (f)''' : urticaire.<br>
'''κνίζω (verbe)''' : Gratter. ; taillader.<br>
'''κοϐαλεία, -ας (nom commun) (f)''' : .<br>
'''κοϐαλεύω (verbe)''' : .<br>
'''κοϐαλίκευμα, -ύματος (nom commun) (n)''' : .<br>
'''κοϐαλισμός, -οῦ (nom commun) (m)''' : .<br>
'''κόϐαλος, -άλου (nom commun) (m)''' : Chenapan ; garnement. Gobelin.<br>
'''κοθαρός, -ά, -όν (adjectif)''' : Forme dorienne de ''καθαρός''.<br>
'''κόθαρος, -ά, -όν (adjectif)''' : Forme éolienne de ''καθαρός''.<br>
'''κοιλάς, -δος (nom commun) (f)''' : vallée.<br>
'''κοῖλος, -ίλη, -ῖλον (adjectif)''' : creux.<br>
'''κοῖλος, -ίλου (nom commun) (m)''' : creux.<br>
'''κοιμάω (verbe)''' : (Actif) Mettre sur une couche, mettre au lit. (Passif) Faire reposer, faire dormir. (Passif, par euphémisme) Faire mourir. (Figuré) Assoupir, apaiser, calmer.<br>
'''κοιμέω (verbe)''' : Forme ionienne de ''κοιμάω''.<br>
'''κοιμητήριον, -ου (nom commun) (n)''' : Dortoir ; cimetière.<br>
'''κοιμίζω (verbe)''' : endormir.<br>
'''κοινοϐουλευτικός, -ή, -όν (adjectif)''' : parlementaire.<br>
'''κοινοϐούλιον, -ίου (nom commun) (n)''' : parlement.<br>
'''κοινός, -ή, -όν (adjectif)''' : commun.<br>
'''κοινότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''κοινός''.<br>
'''κοινότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''κοινός''.<br>
'''κοινότης, -τος (nom commun) (f)''' : communauté.<br>
'''κοινωνία, -ας (nom commun) (f)''' : société.<br>
'''κοινωνικός, -ή, -όν (adjectif)''' : social.<br>
'''κοινωνικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''κοινωνικός''.<br>
'''κοινωνικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''κοινωνικός''.<br>
'''κοινωνικῶς (adverbe)''' : communément.<br>
'''κοινωνικώτατος (adverbe)''' : Superlatif de ''κοινωνικῶς''.<br>
'''κοινωνικώτερος (adverbe)''' : Comparatif de ''κοινωνικῶς''.<br>
'''κοινῶ (verbe)''' : Communiquer. Rendre commun à, faire savoir. Mettre en communication, unir. Rendre commun à tous, prostituer, profaner, souiller.
Unir, assembler, ajuster (une pièce d'une construction). (Moyen) Communiquer, mettre en commun. <br>
'''κοῖος, -ίη, -ῖον (adjectif)''' : Forme ionienne de ''ποῖος''.<br>
'''κοῖτος, -ίτου (nom commun) (m)''' : Lit, couche.<br>
'''κόκκος, -ου (nom commun) (m)''' : Graine, grain, pépin. (Entomologie) Cochenille, kermès, galle du chêne kermès. (par analogie) Pilule. (par analogie) Testicules.<br>
'''κόλλαθον, -άθου (nom commun) (n)''' : .<br>
'''κολοιός, -οῦ (nom commun) (m)''' : choucas.<br>
'''κόκκυξ, -υγος (nom commun) (m)''' : coucou, coccyx.<br>
'''κόκκυ (onomatopée)''' : cri du coucou.<br>
'''κωκύω (verbe)''' : crier, se lamenter.<br>
'''κολακεία, -ας (nom commun) (f)''' : flatterie.<br>
'''κολακευτικός, -ή, -όν (adjectif)''' : flatteur.<br>
'''κολακευτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κολακευτικός''.<br>
'''κολακευτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κολακευτικός''.<br>
'''κολακευτικῶς (adverbe)''' : flatteusement.<br>
'''κολακεύω (verbe)''' : flatter.<br>
'''κόλαξ, -κος (nom commun) (m)''' : flatteur.<br>
'''κολάπτω (verbe)''' : frapper.<br>
'''κόλασις, -άσεως (nom commun) (f)''' : punition.<br>
'''κολαφίζω (verbe)''' : gifler.<br>
'''κόλαφος, -άφου (nom commun) (m)''' : gifle.<br>
'''κολεός, -οῦ (nom commun) (m)''' : Fourreau ; vagin.<br>
'''κόλλα, -ης (nom commun) (f)''' : colle.<br>
'''κόλλαϐος, -άϐου (nom commun) (m)''' : Gâteau, petit pain. Cheville qui attache les cordes de la lyre.<br>
'''κολλάω (verbe)''' : coller.<br>
'''κόλλιξ, -κος (nom commun) (m)''' : petit pain.<br>
'''κολλύρα, -ας (nom commun) (f)''' : petit pain.<br>
'''κολλῶ (verbe)''' : .<br>
'''κολοϐός, -ή, -όν (adjectif)''' : écorné, tronqué ; mutilé.<br>
'''κολοιός, -οῦ (nom commun) (m)''' : geai.<br>
'''κόλον, -ου (nom commun) (n)''' : membre, extrémité ; côlon.<br>
'''κολοκύνθη, - (nom commun) (f)''' : .<br>
'''κολοκυνθίς, - (nom commun) (f)''' : .<br>
'''κολοσσός, -οῦ (nom commun) (m)''' : colosse.<br>
'''κολοττός, -οῦ (nom commun) (m)''' : Forme attique de ''κολοσσός''.<br>
'''κολοφών, -ῶνος (nom commun) (m)''' : Sommet. (Figuré) Achèvement, couronnement. Balle de jeu.<br>
'''κόλπος, -ου (nom commun) (m)''' : (Anatomie) Sein (de la mère ou de la nourrice). Utérus. Pli, creux d’un vêtement. Repli, renfoncement de la mer entre deux vagues. Sein de la terre, d’où Enfers. Golfe. Gouffre, cavité ; vallée profonde.<br>
'''κολχικός, -ή, -όν (adjectif)''' : colchien.<br>
'''κολώνη, -ης (nom commun) (f)''' : monticule, tertre ; tumulus.<br>
'''κομϐολόγιον, -ίου (nom commun) (n)''' : bande de nœuds.<br>
'''κόμϐος, -ου (nom commun) (m)''' : nœud.<br>
'''κοµικός, -ή, -όν (adjectif)''' : comique.<br>
'''κοµικότατα, -, - (adverbe)''' : Superlatif de ''κοµικῶς''.<br>
'''κοµικότερον, -, - (adverbe)''' : Comparatif de ''κοµικῶς''.<br>
'''κοµικῶς (adverbe)''' : comiquement.<br>
'''κοµικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κοµικός''.<br>
'''κοµικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κοµικός''.<br>
'''κομπάζω (verbe)''' : hâbler.<br>
'''κομπαστής, -οῦ (nom commun) (m)''' : hâbleur.<br>
'''κομψός, -ή, -όν (adjectif)''' : élégant.<br>
'''κομψότατα, -, - (adverbe)''' : Superlatif de ''κομψῶς''.<br>
'''κομψότερον, -, - (adverbe)''' : Comparatif de ''κομψῶς''.<br>
'''κομψότης, -τος (nom commun) (f)''' : élégance.<br>
'''κομψῶς (adverbe)''' : élégamment.<br>
'''κομψώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κομψός''.<br>
'''κομψώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κομψός''.<br>
'''κονδυλισμίς, -δος (nom commun) (f)''' : chiquenaude ; pichenette.<br>
'''κόνδυ, -ος (nom commun) (n)''' : somm e.<br>
'''κονία, -ας (nom commun) (f)''' : poussière.<br>
'''κονίη, -ης (nom commun) (f)''' : Forme homérique et ionienne de ''κονία''.<br>
'''κόνικλος, -ίκλου (nom commun) (m)''' : lapin.<br>
'''κόνις, -εως (nom commun) (f)''' : cendre ; poussière.<br>
'''κονίς, -δος (nom commun) (f)''' : lente.<br>
'''κοντά (adverbe)''' : près ; à proximité.<br>
'''κοντινός, -ή, -όν (adjectif)''' : proche.<br>
'''κοντινῶς (adverbe)''' : .<br>
'''κοντινώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κοντινός''.<br>
'''κοντινώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κοντινός''.<br>
'''κογχύλιον, -ίου (nom commun) (n)''' : coquillage.<br>
'''κοπριά, -ᾶς (nom commun) (f)''' : fumier.<br>
'''κόπρος, -ου (nom commun) (m)''' : excrément.<br>
'''κόρα, -ας (nom commun) (f)''' : Forme éolienne de ''κόρη''.<br>
'''κόραξ, -κος (nom commun) (m)''' : corbeau.<br>
'''κορέννυμι (verbe)''' : rassasier ; (Au passif et au moyen) Être rassasié. Avoir le dégoût de.<br>
'''κόρευμα, -ύματος (nom commun) (n)''' : virginité.<br>
'''κορεύομαι (verbe)''' : être vierge.<br>
'''κόρη, -ης (nom commun) (f)''' : jeune fille.<br>
'''κορίαννον, -άννου (nom commun) (n)''' : coriandre.<br>
'''κορίζομαι (verbe)''' : caresser.<br>
'''κορμός, -οῦ (nom commun) (m)''' : .<br>
'''κόρος, -ου (nom commun) (m)''' : Jeune garçon ; satiété, dédain.<br>
'''κόρυζα, -ης (nom commun) (f)''' : coryza.<br>
'''κόρυς, -θος (nom commun) (f)''' : Tête ; casque.<br>
'''κορώνη, -ης (nom commun) (f)''' : corneille.<br>
'''κορωνός, -ή, -όν (adjectif)''' : courbe ; recourbé.<br>
'''κόσμημα, -ήματος (nom commun) (n)''' : bijou.<br>
'''κοσμητικός, -ή, -όν (adjectif)''' : Décoratif ; ordonné.<br>
'''κοσμητής, -οῦ (nom commun) (m)''' : Ordonnateur ; arrangeur.<br>
'''κόσμος, -ου (nom commun) (m)''' : Ordre. Monde ; univers.<br>
'''κόσσυκος, -ύκου (nom commun) (m)''' : Forme de ''κόσσυϕος''.<br>
'''κόσσυϕος, -ύϕου (nom commun) (m)''' : merle.<br>
'''κόσος, -η, -ον (adjectif)''' : Forme ionienne de ''πόσος''.<br>
'''κόττος, -ου (m)''' : coq.<br>
'''κόττυϕος, -ύϕου (nom commun) (m)''' : Forme attique de ''κόσσυϕος''.<br>
'''κοτύλη, -ης (nom commun) (f)''' : écuelle.<br>
'''κουρεύς, -έως (nom commun) (m)''' : barbier.<br>
'''κουρεῖον, -ίου (nom commun) (n)''' : boutique du barbier.<br>
'''κούρη, -ης (nom commun) (f)''' : Forme ionienne de ''κόρη''.<br>
'''κοῦρος, -ύρου (nom commun) (m)''' : Forme ionienne de ''κόρος''.<br>
'''κοῦ (adverbe interrogatif)''' : Forme ionienne de ''ποῦ''.<br>
'''κοῦφος, -ύφη, -ῦφον (adjectif)''' : léger.<br>
'''κουφότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''κοῦφος''.<br>
'''κουφότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''κοῦφος''.<br>
'''κούφως (adverbe)''' : légèrement.<br>
'''κόφινος, -ίνου (nom commun) (m)''' : corbeille.<br>
'''κοχλιάριον, -ίου (nom commun) (n)''' : cuillère.<br>
'''κοχλίας, -ου (nom commun) (m)''' : escargot.<br>
'''κρα (onomatopée)''' : croa.<br>
'''κράϐατος, -άτου (nom commun) (m)''' : Forme macédonienne de ''κράϐϐατος''.<br>
'''κράϐϐατος, -άτου (nom commun) (m)''' : grabat.<br>
'''κραιπάλη, -ης (nom commun) (f)''' : ivresse.<br>
'''κραίνω (verbe)''' : Commander, gouverner.<br>
'''κράμϐη, -ης (nom commun) (f)''' : chou.<br>
'''κράμϐος, -η, -ον (f)''' : fort (en parlant de la voix ou du son).<br>
'''κρανίον, -ου (nom commun) (n)''' : crâne.<br>
'''κρανοκοπέω (verbe)''' : décapiter.<br>
'''κράνος, -ους (nom commun) (n)''' : casque.<br>
'''κρᾶσις, -άσεως (nom commun) (n)''' : mélange de deux ou plusieurs choses.<br>
'''κράτειρα, -ίρας (nom commun) (f)''' : dirigeante.<br>
'''κράτος, -ους (nom commun) (n)''' : Force du corps, vigueur, solidité. Domination, puissance.<br>
'''κράτωρ, -ορος (nom commun) (m)''' : dirigeant.<br>
'''κραυγάζω (verbe)''' : crier.<br>
'''κραυγή, -ῆς (nom commun) (f)''' : cri.<br>
'''κρέας, -ατος (nom commun) (n)''' : viande.<br>
'''κρεῖας, -ίατος (nom commun) (n)''' : Forme homérique de ''κρέας''.<br>
'''κρείουσα, -ας (nom commun) (f)''' : cri.<br>
'''κρείσσων, -ων, -ον (adjectif)''' : maîtresse ; souveraine.<br>
'''κρείων, -οντος (nom commun) (m)''' : seigneur, maître ; souverain.<br>
'''κρέξ, -κου (nom commun) (m)''' : râle (oiseau).<br>
'''κρέων, -οντος (nom commun) (m)''' : Forme homérique de ''κρείων''.<br>
'''κρημνός, -οῦ (nom commun) (m)''' : falaise, précipice.<br>
'''κρηναῖος, -ία, -ῖον (adjectif)''' : .<br>
'''κρήνη, -ης (nom commun) (m)''' : Source, puits ; fontaine. (Poétique) (Au pluriel) Eau.<br>
'''κρηπίς, -ῖδος (nom commun) (f)''' : pantoufle.<br>
'''κρῆσις, -ήσεως (nom commun) (n)''' : Forme ionienne de ''κρᾶσις''.<br>
'''κρῆς, -ήτος (nom commun) (n)''' : Forme dorienne de ''κρέας''.<br>
'''κρίϐανος, -άνου (nom commun) (m)''' : braséro ; four à pain.<br>
'''κρίκος, -ου (nom commun) (m)''' : anneau.<br>
'''κρῖμα, -ίματος (nom commun) (n)''' : Objet de contestation, contestation, querelle. (Par suite) Jugement, décision judiciaire. Condamnation, peine. (Par extension) Jugement, Décision. Action de juger.<br>
'''κριός, -οῦ (nom commun) (m)''' : bélier.<br>
'''κρίνω (verbe)''' : décider.<br>
'''κρίσις, -ίσεως (nom commun) (f)''' : décision.<br>
'''κριτήριον, -ίου (nom commun) (n)''' : test.<br>
'''κριτήρ, -ῆρος (nom commun) (m)''' : interprète.<br>
'''κριτής, -οῦ (nom commun) (m)''' : juge.<br>
'''κριτικός, -ή, -όν (adjectif)''' : décisif.<br>
'''κριτικῶς (adverbe)''' : décisivement.<br>
'''κριτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κριτικός''.<br>
'''κριτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κριτικός''.<br>
'''κροκόδειλος, -ίλου (nom commun) (m)''' : crocodile.<br>
'''κρόμμυον, -ύου (nom commun) (n)''' : ognon.<br>
'''κρόμυον, -ύου (nom commun) (n)''' : Variante de ''κρόμμυον''.<br>
'''κρόταφος, -άφου (nom commun) (m)''' : tempe.<br>
'''κροῦσις, -ύσεως (nom commun) (f)''' : Choc, collision ; impact, percussion.<br>
'''κρούω (verbe)''' : heurter, choquer. Frapper l’un contre l’autre. Frapper les cordes d’un instrument. Heurter ou pousser pour mettre en mouvement. Heurter avec le doigt pour l’éprouver et le faire résonner. (Figuré) Piquer, chatouiller en parlant de sensations physiques.<br>
'''κρυμός, -οῦ (nom commun) (m)''' : gel.<br>
'''κρύος, -ους (nom commun) (n)''' : froid glacial. Frisson (de crainte).<br>
'''κρύσταλλος, -άλλου (nom commun) (m)''' : Eau congelée ; verre transparent.<br>
'''κροκόδειλος, -ίλου (nom commun) (m)''' : crocodile.<br>
'''κρόκος, -ου (nom commun) (m)''' : safran.<br>
'''κρυπτός, -ός, -όν (adjectif)''' : caché, secret.<br>
'''κρύπτω (verbe)''' : cacher.<br>
'''κρυφᾷ (adverbe)''' : Forme dorienne de ''κρυφῇ''.<br>
'''κρυφῇ (adverbe)''' : secrètement.<br>
'''κρύφος, -ου (nom commun) (f)''' : Action de cacher ; cachette.<br>
'''κρωγμός, -οῦ (nom commun) (m)''' : croassement.<br>
'''κρώζω (verbe)''' : croasser.<br>
'''κρωσσίον, -ου (nom commun) (n)''' : cruche.<br>
'''κρωσσός, -οῦ (nom commun) (m)''' : jarre ; urne.<br>
'''κτείνω (verbe)''' : tuer.<br>
'''κτείς, -ενός (nom commun) (m)''' : Peigne. Râteau. (Au pluriel) Doigts.<br>
'''κτένιον, -ίου (nom commun) (m)''' : Diminutif de ''κτείς''.<br>
'''κτῆνος, -ήνους (nom commun) (n)''' : (Au pluriel) Bétail. (Au singulier) Bête domestique.<br>
'''κτίριον, -ίου (nom commun) (n)''' : bâtiment.<br>
'''κτυπέω (verbe)''' : claquer.<br>
'''κτύπημα, -ήματος (nom commun) (n)''' : claquement.<br>
'''κτῶμαι (verbe)''' : acquérir.<br>
'''κυανός, -ή, -όν (adjectif)''' : bleu.<br>
'''κυϐέρνησις, -ήσεως (nom commun) (f)''' : pilotage ; gouvernement.<br>
'''κυϐερνήτης, -ου (nom commun) (m)''' : pilote ; gouverneur.<br>
'''κυϐερνητικός, -ή, -όν (adjectif)''' : pilotable ; gouvernemental.<br>
'''κυϐερνισμός, -οῦ (nom commun) (m)''' : pilotage ; gouvernement.<br>
'''κυϐερνῶ (verbe)''' : piloter ; gouverner.<br>
'''κῦδος, -ύδους (nom commun) (n)''' : gloire ; renommée.<br>
'''κυῶ (verbe)''' : .<br>
'''κύημα, -ήματος (nom commun) (n)''' : vague.<br>
'''κύησις, -ήσεως (nom commun) (f)''' : grossesse.<br>
'''κύκλος, -ου (nom commun) (m)''' : cercle ; rond.<br>
'''κυκλῶ (verbe)''' : Tourner, rouler. Envelopper, cerner.<br>
'''κύκλωμα, -ώματος (nom commun) (n)''' : circuit.<br>
'''κυκλών, -ῶνος (nom commun) (m)''' : cyclone.<br>
'''κύκνος, -ου (nom commun) (m)''' : cygne.<br>
'''κυλινδρικός, -ή, -όν (adjectif)''' : cylindrique.<br>
'''κύλινδρος, -ίνδρου (nom commun) (m)''' : cylindre.<br>
'''κυλίνδω (verbe)''' : rouler.<br>
'''κύλιξ, -κος (nom commun) (f)''' : coupe (récipient).<br>
'''κυλλός, -ή, -όν (adjectif)''' : Forme chypriote de ''χωλός''.<br>
'''κῦμα, -ύματος (nom commun) (n)''' : onde ; vague.<br>
'''κυναλώπηξ, -εκος (nom commun) (f)''' : .<br>
'''κυνηγός, -οῦ (nom commun) (m)''' : chasseur.<br>
'''κυνηγῶ (verbe)''' : chasser.<br>
'''κύνικλος, -ίκλου (nom commun) (m)''' : Forme de ''κόνικλος''.<br>
'''κυνικός, -ή, -όν (adjectif)''' : canin ; (Philosophie) cynique.<br>
'''κυνόροδον, -όδου (nom commun) (n)''' : églantier des haies.<br>
'''κυπάρισσος, -ίσσου (nom commun) (f)''' : cyprès.<br>
'''κυπάριττος, -ίττου (nom commun) (f)''' : Forme attique de ''κυπάρισσος''.<br>
'''κυπρῖνος, -ίνου (nom commun) (m)''' : carpe.<br>
'''κύπτω (verbe)''' : se baisser en avant. Baisser la tête (ou les yeux) de honte.<br>
'''κυρία, -ας (nom commun) (f)''' : maîtresse ; souveraine.<br>
'''κυριακός, -ή, -όν (adjectif)''' : seigneurial.<br>
'''κύριος, -ίου (nom commun) (m)''' : maître ; souverain.<br>
'''κύρος, -ους (nom commun) (n)''' : prestige.<br>
'''κῦρος, -ύρου (nom commun) (m)''' : décret.<br>
'''κῦρρος, -ύρρου (nom commun) (m)''' : Forme thessalienne de ''κύριος''.<br>
'''κύρωσις, -ώσεως (nom commun) (f)''' : sanction.<br>
'''κυρῶ (verbe)''' : confirmer, ratifier ; sanctionner.<br>
'''κυσός, -οῦ (nom commun) (m)''' : sexe de la femme.<br>
'''κύστιγξ, -γος (nom commun) (f)''' : vésicule.<br>
'''κύστις, -εως (nom commun) (f)''' : sac ; (Au pluriel) Poches sous les yeux.<br>
'''κύσσω (verbe)''' : Forme poétique de ''κύσω''.<br>
'''κύσω (verbe)''' : donner un baiser.<br>
'''κύτος, -ους (nom commun) (n)''' : Creux d'un navire, d'un bouclier, d'une cuirasse. Objet creux. Qui recouvre ou enveloppe.<br>
'''κυφός, -ή, -όν (adjectif)''' : Arqué, bossu. Courbé.<br>
'''κυφότατος, -έρα, -ότερον (adjectif)''' : Comparatif de ''κυφός''.<br>
'''κυφότερος, -άτη, -ότατον (adjectif)''' : Superlatif de ''κυφός''.<br>
'''κύφων, -ονος (nom commun) (m)''' : pilori.<br>
'''κύφωσις, -ώσεως (nom commun) (f)''' : .<br>
'''κυφῶς (adverbe)''' : .<br>
'''κύψελος, -έλου (nom commun) (m)''' : martinet (oiseau).<br>
'''κύων, -νός (nom commun) (m/f)''' : chien ; chienne.<br>
'''κωδωνίζω (verbe)''' : sonner les cloches.<br>
'''κωδώνιον, -ίου (nom commun) (n)''' : clochette.<br>
'''κωδωνόκροτος, - ()''' : .<br>
'''κωδωνοφαλαρόπωλος, - ()''' : .<br>
'''κωδωνοφορέω (verb)''' : .<br>
'''κώδων, -ος (nom commun) (m)''' : cloche.<br>
'''κωκύω (verbe)''' : .<br>
'''κώληψ, -πος (nom commun) (f)''' : jarret.<br>
'''κωλοϐαθριστής, -οῦ (nom commun) (m)''' : Acrobate marchant sur des échasses.<br>
'''κωλόϐαθρον, -άθρου (nom commun) (n)''' : échasse.<br>
'''κῶλον, -ώλου (nom commun) (n)''' : .<br>
'''κώμη, -ης (nom commun) (f)''' : village ; quartier.<br>
'''κωμικός, -ή -όν (adjectif)''' : comique.<br>
'''κωµικότατα, -, - (adverbe)''' : Superlatif de ''κωµικῶς''.<br>
'''κωµικότερον, -, - (adverbe)''' : Comparatif de ''κωµικῶς''.<br>
'''κωµικῶς (adverbe)''' : comiquement.<br>
'''κωµικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''κωµικός''.<br>
'''κωµικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''κωµικός''.<br>
'''κῶμος, -ώμου (nom commun) (m)''' : Festin, banquet. (Par extension) Troupe impétueuse (en parlant des Érinyes), bande.<br>
'''κωμόπολις, -όλεως (nom commun) (n)''' : bourgade.<br>
'''κωμῳδία, -ας (nom commun) (f)''' : poésie satirique.<br>
'''κωνωπεῖον, -ίου (nom commun) (n)''' : moustiquaire.<br>
'''κώνωψ, -ωπος (nom commun) (m)''' : cousin (genre de moustique) ; moucheron.<br>
'''κώπα, -ας (nom commun) (f)''' : Forme dorienne de ''κώπη''.<br>
'''κωπέω (f)''' : ramer.<br>
'''κώπη, -ης (nom commun) (f)''' : Anse, poignée (d’une rame), manche. Rame.<br>
'''κωπίον, -ίου (nom commun) (n)''' : Diminutif de ''κώπη''.<br>
'''κώρα, -ας (nom commun) (f)''' : Forme dorienne de ''κόρη''.<br>
'''κῶρος, -ώρου (nom commun) (m)''' : Forme dorienne de ''κόρος''.<br>
'''κῶς (adverbe)''' : Forme ionienne de ''πῶς''.<br>
'''κώταλις, - (nom commun) (f)''' : cuillère.<br>
'''κωφός, -ή -όν (adjectif)''' : sourd.<br>
'''Καικιλία, -ας (nom propre) (f)''' : Cécile.<br>
'''Καικίλιος, -ίου (nom propre) (m)''' : Cécilius.<br>
'''Κάϊν (nom propre) (m)''' : Caïn.<br>
'''Καινὴ Διαθήκη (locution nominale) (f)''' : Nouveau Testament.<br>
'''Καῖσαρ, -ίσαρος (nom propre) (m)''' : César.<br>
'''Καλλίας, -ου (nom propre) (m)''' : Callias.<br>
'''Καλλικρατίδας, -ου (nom propre) (m)''' : Callicratidas.<br>
'''Καλλίμαχος, -άχου (nom propre) (m)''' : Callimaque.<br>
'''Καλλιόπη, -ης (nom propre) (f)''' : Calliope.<br>
'''Καλλιρόη, -ης (nom propre) (f)''' : Callirhoé.<br>
'''Καλλιστώ, -οῦς (nom propre) (f)''' : Callisto.<br>
'''Καλυψώ, -οῦς (nom propre) (f)''' : Calypso.<br>
'''Καμϐύσης, -ου (nom propre) (m)''' : Cambyse.<br>
'''Κανδάκη, -ης (nom propre) (f)''' : Candace.<br>
'''Καρδοῦχος, -ύχου (nom commun) (m)''' : Kurde.<br>
'''Καρκίνος, -ου (nom propre) (m)''' : Cancer.<br>
'''Κᾶρ, -άρος (nom propre) (f)''' : Forme éolienne de ''Κήρ''.<br>
'''Καρχηδόνιος, -ίου (nom commun) (m)''' : Carthaginois.<br>
'''Καρχηδών, -όνος (nom propre) (f)''' : Carthage.<br>
'''Κασσάνδρα, -ας (nom propre) (f)''' : Cassandre.<br>
'''Κάσσανδρος, -άνδρου (nom commun) (m)''' : Cassandre.<br>
'''Κεϐρίονης, -àου (nom commun) (m)''' : Cébrion.<br>
'''Κελτίς, -δος (nom commun) (f)''' : Celte.<br>
'''Κελτός, -οῦ (nom commun) (m)''' : Celte.<br>
'''Κερασούντιος, -α, -ον (adjectif)''' : cérasien.<br>
'''Κερασοῦς, -ῦντος (nom commun) (m)''' : Cérasus.<br>
'''Κέρϐερος, -έρου (nom propre) (m)''' : Cerbère.<br>
'''Κέρκωψ, -πος (nom propre) (m)''' : Cercops.<br>
'''Κήρ, -ός (nom propre) (f)''' : Une des Kères.<br>
'''Κηφισιά, -ᾶς (nom propre) (f)''' : Céphisia.<br>
'''Κικέρων, -ος (nom propre) (m)''' : Cicéron.<br>
'''Κιλικία, -ας (nom propre) (f)''' : Cilicie.<br>
'''Κίρκη, -ης (nom propre) (f)''' : Circé.<br>
'''Κλεάνθης, -ου (nom propre) (m)''' : Cléante.<br>
'''Κλεισθένης, -ους (nom propre) (m)''' : Clisthène.<br>
'''Κλείτανδρος, -άνδρου (nom propre) (m)''' : Clitandre.<br>
'''Κλειτόμαχος, -άχου (nom propre) (m)''' : Clitomaque.<br>
'''Κλεῖτος, -ίτου (nom propre) (m)''' : Cleithos.<br>
'''Κλειτοφῶν, -τος (nom propre) (m)''' : Clitophon.<br>
'''Κλειώ, -οῦς (nom propre) (f)''' : Clio.<br>
'''Κλεoμήδης, -ους (nom propre) (m)''' : Cléomède.<br>
'''Κλεοπᾶς, -ᾶ (nom propre) (m)''' : Cléopas.<br>
'''Κλεοπάτρα, -ας (nom propre) (f)''' : Cléopâtre.<br>
'''Κλεόπατρος, -άτρου (nom propre) (m)''' : Cléopatros.<br>
'''Κλεώνυμος, -ύμου (nom propre) (m)''' : Cléonyme.<br>
'''Κλωθώ, -οῦς (nom propre) (f)''' : Clotho (première Moire).<br>
'''Κόϊντος, -ΐντου (nom propre) (m)''' : Quentin.<br>
'''Κοῖος, -ίου (nom propre) (m)''' : Céos.<br>
'''Κολοφών, -ῶνος (nom commun) (m)''' : Colophon (ville).<br>
'''Κολχίς, -δος (nom propre) (f)''' : Colchide.<br>
'''Κολχός, -οῦ (nom commun) (m)''' : Colchien.<br>
'''Κόνων, -ος (nom commun) (m)''' : Conon.<br>
'''Κόραμα, -ας (nom propre) (f)''' : Göreme.<br>
'''Κορίνθιος, -ίου (nom commun) (f)''' : Corinthien.<br>
'''Κόρινθος, -ίνθου (nom propre) (f)''' : Corinthe.<br>
'''Κορσίς, -δος (nom propre) (f)''' : Corse.<br>
'''Κόρκυρα, -ύρας (nom propre) (f)''' : Corcyre.<br>
'''Κορυφώ, -οῦς (nom propre) (m)''' : Corfou (île).<br>
'''Κότταλος, -άλου (nom propre) (m)''' : Cottalos.<br>
'''Κουῥάν (nom propre) (m)''' : Coran.<br>
'''Κράτος, -ους (nom propre) (m)''' : Cratos.<br>
'''Κρατύλος, -ου (nom propre) (m)''' : Cratyle.<br>
'''Κρεῖος, -ίου (nom propre) (m)''' : Crios.<br>
'''Κρέουσα, -ας (nom propre) (f)''' : Créuse.<br>
'''Κρέων, -οντος (nom propre) (m)''' : Créon.<br>
'''Κριός, -οῦ (nom propre) (m)''' : Bélier.<br>
'''Κρίτοϐουλος, -ύλου (nom propre) (m)''' : Critobule.<br>
'''Κροῖσος, -ίσου (nom propre) (m)''' : Crésus.<br>
'''Κρίτοϐουλος, -ύλου (nom propre) (m)''' : Critobule.<br>
'''Κροκοδειλόπολις, -όλεως (nom propre) (f)''' : Fayoum.<br>
'''Κτησιφῶν, -τος (nom propre) (m)''' : Ctésiphon.<br>
'''Κτιμένη, -ης (nom propre) (f)''' : Ctimène (sœur d'Ulysse).<br>
'''Κυαξάρης, -ου (nom propre) (m)''' : Cyaxare.<br>
'''Κύθηρα, -ήρων (nom propre) (n)''' : Cythère.<br>
'''Κυθήριος, -ίου (nom commun)''' : Cythérien.<br>
'''Κύκλωψ, -ωπος (nom propre) (m)''' : Cyclope.<br>
'''Κυνόσαργες, -ων (nom propre) (m)''' : Cynosarges.<br>
'''Κύπριος, -ίου (nom commun) (m)''' : Chypriote.<br>
'''Κύπρος, -ου (nom propre) (f)''' : Chypre.<br>
'''Κύριλλος, -ίλλου (nom propre) (m)''' : Cyrille.<br>
'''Κύριος, -ίου (nom propre) (m)''' : Seigneur.<br>
'''Κύρνιος, -ίου (nom commun) (m)''' : Corse.<br>
'''Κύρνος, -ου (nom propre) (f)''' : Corse.<br>
'''Κῦρος, -ύρου (nom propre) (m)''' : Cyrus.<br>
'''Κύψελος, -έλου (nom propre) (m)''' : Cypsélos.<br>
'''Κωκυτός, -τοῦ (nom propre) (m)''' : Cocyte.<br>
'''Κωνσταντῖνος, -ίνου (nom propre) (m)''' : Constantin.<br>
'''Κωνσταντινούπολις, -όλεως (nom propre) (f)''' : Constantinople.<br>
==Λ==
'''λαϐή, -ῆς (nom commun) (f)''' : anse.<br>
'''λάϐρυς, -εως (nom commun) (f)''' : labrys.<br>
'''λαϐύρινθος, -ίνθου (nom commun) (m)''' : labyrinthe.<br>
'''λάγανον, -άνου (nom commun) (n)''' : crêpe.<br>
'''λαγαρός, -ά, -όν (adjectif)''' : Creux, enfoncé ; lâche, ample.<br>
'''λαγνεία, -ας (nom commun) (f)''' : libertinage.<br>
'''λάγνευμα, -ύματος (nom commun) (n)''' : coït (acte).<br>
'''λαγνεύω (verbe)''' : coïter.<br>
'''λάγνης, -ης, -ες (adjectif)''' : Forme attique de ''λάγνος''.<br>
'''λάγνος, -α, -ον (adjectif)''' : libertin, débauché.<br>
'''λαγός, -οῦ (nom commun) (m)''' : lièvre.<br>
'''λάγυνος, -ύνου (nom commun) (m/f)''' : flacon, pichet.<br>
'''λαγών, -όνος (nom commun) (m)''' : flanc.<br>
'''λαγώς, -ώ (nom commun) (m)''' : Forme attique de ''λαγός''.<br>
'''λᾴα -ας (nom commun) (f)''' : Forme dorienne de ''λεία''.<br>
'''λάθος, -ους (nom commun) (n)''' : erreur.<br>
'''λαϊκός, -ή, -όν (adjectif)''' : populaire.<br>
'''λαϊκῶς (adverbe)''' : populairement.<br>
'''λαϊκώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''λαϊκός''.<br>
'''λαϊκώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''λαϊκός''.<br>
'''λαϊκώτατα, -, - (adverbe)''' : Superlatif de ''λαϊκῶς''.<br>
'''λαϊκώτερον, -, - (adverbe)''' : Comparatif de ''λαϊκῶς''.<br>
'''λαιµός, -οῦ (nom commun) (m)''' : cou, gorge.<br>
'''λαῖον, -ίου (nom commun) (n)''' : faux.<br>
'''λαιός, -ά, -όν (adjectif)''' : qui est à gauche.<br>
'''λαιψηρός, -ά, -όν (adjectif)''' : agile, véhément.<br>
'''λαιψηρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''λαιψηρός''.<br>
'''λαιψηρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''λαιψηρός''.<br>
'''λαιψηρῶς (adverbe)''' : agilement, véhémentement.<br>
'''λάκτισμα, -ίσματος (nom commun) (n)''' : coup de pied.<br>
'''λάκυθος, -ύθου (nom commun) (f)''' : Forme dorienne de ''λήκυθος''.<br>
'''λακωνικός, -ή, -όν (adjectif)''' : concis.<br>
'''λακωνικότης, -τος (nom commun) (f)''' : concision.<br>
'''λακωνικῶς (adverbe)''' : concisément.<br>
'''λακωνικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''λακωνικός''.<br>
'''λακωνικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''λακωνικός''.<br>
'''λᾳστής, -οῦ (nom commun) (m)''' : Forme dorienne de ''λῃστής''.<br>
'''λαλέω (verbe)''' : Babiller, bavarder.<br>
'''λαλιά, -ᾶς (nom commun) (f)''' : Babil, bavardage.<br>
'''λάλος, -ος, -ον (adjectif)''' : Babillard, bavard.<br>
'''λαμϐάνω (verbe)''' : prendre.<br>
'''λάμϐδα (nom commun) (n)''' : lambda.<br>
'''λαμπάς, -δος (nom commun) (f)''' : torche.<br>
'''λαμπέτης, -ου (nom commun) (m)''' : .<br>
'''λαμπέτις, -δος (nom commun) (f)''' : .<br>
'''λάμπη, -ης (nom commun) (f)''' : Forme homérique et ionienne de ''λαμπάς''.<br>
'''λαμπηδών, -όνος (nom commun) (f)''' : brillance des yeux.<br>
'''λαμπρός, -ά, -όν (adjectif)''' : brillant.<br>
'''λαμπρῶς (verbe)''' : brillamment.<br>
'''λαμπρώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''λαμπρός''.<br>
'''λαμπρώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''λαμπρός''.<br>
'''λαμπτήρ, -ῆρος (nom commun) (m)''' : briquet, lanterne ; torche.<br>
'''λαμπυρίς, -δος (nom commun) (f)''' : ver luisant.<br>
'''λάμπω (verbe)''' : briller.<br>
'''λάμψις, -εως (nom commun) (f)''' : brillance des étoiles, des éclairs, du Soleil.<br>
'''λανθάνω (verbe)''' : être caché, faire oublier.<br>
'''λαός, -οῦ (nom commun) (m)''' : peuple.<br>
'''λάριξ, -κος (nom commun) (m)''' : mélèze.<br>
'''λάρυγξ, -γος (nom commun) (m)''' : larynx. (Par suite) Gorge, gosier.<br>
'''λάσανον, -άνου (nom commun) (n)''' : Casserole, marmite ; pot.<br>
'''λατρεία, -ας (nom commun) (f)''' : service à gages. Service d’un dieu ; culte, adoration. Soins à donner au corps ou à l'âme.<br>
'''λατρεύς, -έως (nom commun) (m)''' : serviteur engagé.<br>
'''λατρεύω (verbe)''' : Être serviteur à gages. (Généralement) Servir que ce soit en parlant d’homme libre ou d’esclaves. (En particulier) Être serviteur de Dieu.<br>
'''λάτρης, -ου (nom commun) (n)''' : adorateur.<br>
'''λάτρον, -ου (nom commun) (n)''' : Gage, salaire ; rémunération.<br>
'''λάφυρον, -ύρου (nom commun) (n)''' : butin.<br>
'''λέαινα, -ίνης (nom propre) (f)''' : lionne.<br>
'''λέϐης, -τος (nom commun) (m)''' : chaudière.<br>
'''λέγω (verbe)''' : choisir.<br>
'''λεία, -ας (nom commun) (f)''' : pillage.<br>
'''λείϐω (verbe)''' : verser.<br>
'''λειμών, -ῶνος (nom commun) (m)''' : pré.<br>
'''λειμωνήρης, -ης, -ες (adjectif)''' : .<br>
'''λειμωνιάς, -δος (nom commun) (f)''' : lémoniade.<br>
'''λειμώνιον, -ίου (nom commun) (n)''' : .<br>
'''λειμώνιος, -α, -ον (adjectif)''' : .<br>
'''λειμωνίς, -δος (nom commun) (f)''' : lémoniade.<br>
'''λειμωνοειδής, -ής, -ές (adjectif)''' : .<br>
'''λειμωνόθεν (adverbe)''' : .<br>
'''λεῖος, -ία, -ῖον (adjectif)''' : lisse, uni.<br>
'''λείριον, -ίου (nom commun) (n)''' : lis (fleur).<br>
'''λειτούργημα, -ήματος (nom commun) (n)''' : .<br>
'''λειτουργία, -ας (nom commun) (f)''' : Cérémonie publique, service public.<br>
'''λειτουργός, -οῦ (nom commun) (m)''' : Ministre, fonctionnaire. (Religion) Ministre du culte.<br>
'''λειχήν, -ῆνος (nom commun) (m)''' : Lèpre, dartre sur le corps de l’homme. Cal sur la jambe du cheval. Lichen.<br>
'''λείχω (verbe)''' : lécher.<br>
'''λεξικός, -ή, -όν (adjectif)''' : lexical.<br>
'''λεξικόν, -οῦ (nom commun) (n)''' : dictionnaire.<br>
'''λεξικῶς (adverbe)''' : lexicalement.<br>
'''λεξικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''λεξικός''.<br>
'''λεξικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''λεξικός''.<br>
'''λέξις, -εως (nom commun) (f)''' : Parole, action de parler. Élocution, style, manière de parler.<br>
'''λεόντειος, -ος, -ον (adjectif)''' : léonin.<br>
'''λεπίς, -δος (nom commun) (f)''' : Coque d’œuf. Lamelle de métal.<br>
'''λεπτότης, -τος (nom commun) (f)''' : Minceur. Délicatesse.<br>
'''λεπτός, -ή, -όν (adjectif)''' : Dépouillé de sa peau, de sa pellicule. Mince. Délicat.<br>
'''λεπτότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''λεπτός''.<br>
'''λεπτότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''λεπτός''.<br>
'''λεπτῶς (adverbe)''' : Mincement. Délicatement<br>
'''λέπω (verbe)''' : Peler, écosser ; écorcher.<br>
'''λευγαλέος, -α, -ον (adjectif)''' : Triste, malheureux. Irrité, funeste. Désolé.<br>
'''λευκός, -ή, -όν (adjectif)''' : blanc.<br>
'''λεύκη, -ης (nom commun) (f)''' : peuplier.<br>
'''λεύσσω (verbe)''' : voir.<br>
'''λεώϐατος (nom commun)''' : .<br>
'''λεωλογέω (verbe)''' : .<br>
'''λέων, -οντος (nom commun) (m)''' : lion.<br>
'''λεώς, -ώ (nom commun) (m)''' : Forme attique de ''λαός''.<br>
'''λεωσφέτερος''' : .<br>
'''λεωφόρος, -ου (nom commun) (f)''' : Avenue, boulevard.<br>
'''λήθαιος, -α, -ον (adjectif)''' : .<br>
'''ληθάνω (verbe)''' : faire oublier.<br>
'''ληθαργέω (verbe)''' : oublier.<br>
'''ληθαργία, -ας (nom commun) (f)''' : léthargie.<br>
'''ληθαργικός, -ή, -όν (adjectif)''' : léthargique.<br>
'''ληθαργικῶς (adverbe)''' : léthargiquement.<br>
'''ληθαργικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ληθαργικός''.<br>
'''ληθαργικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ληθαργικός''.<br>
'''ληθαργικώτατα, -, - (adverbe)''' : Superlatif de ''ληθαργικῶς''.<br>
'''ληθαργικώτερον, -, - (adverbe)''' : Comparatif de ''ληθαργικῶς''.<br>
'''λήθαργος, -άργου (nom commun) (m)''' : torpeur.<br>
'''λήθη, -ης (nom commun) (f)''' : oubli.<br>
'''ληΐη, -ης (nom commun) (f)''' : Forme ionienne de ''λεία''.<br>
'''ληΐς, - (nom commun) (f)''' : Forme homérique de ''λεία''.<br>
'''ληϊστής, -οῦ (nom commun) (m)''' : Forme ionienne de ''λῃστής''.<br>
'''λήιτον, -ίτου (nom commun) (n)''' : (En Achaïe) mairie.<br>
'''λήκυθος, -ύθου (nom commun) (f)''' : petit vase.<br>
'''λῃστής, -οῦ (nom commun) (m)''' : voleur, brigand. (en particulier) Pirate.<br>
'''λῆξις, -ήξεως (nom commun) (f)''' : tirage au sort.<br>
'''λῆμμα, -ήμματος (nom commun) (n)''' : Bénéfice, salaire, recette. Gain, profit. Bénéfice.<br>
'''λημματιστής, -οῦ (nom commun) (m)''' : receveur.<br>
'''λῆνος, -ήνους (nom commun) (n)''' : laine.<br>
'''ληός, -οῦ (nom commun) (m)''' : Forme ionienne de ''λαός''.<br>
'''λησμοσύνη, -ης (nom commun) (f)''' : oubli.<br>
'''λῆψις, -ήψεως (nom commun) (f)''' : prise.<br>
'''λίϐανος, -άνου (nom commun) (m)''' : oliban.<br>
'''λιϐάς, -δος (nom commun) (f)''' : prairie.<br>
'''λίθος, -ου (nom commun) (m)''' : pierre.<br>
'''λιμήν, -ένος (nom commun) (m)''' : port.<br>
'''λίμνη, -ης (nom commun) (f)''' : lac.<br>
'''λιµός, -οῦ (nom commun) (m)''' : faim.<br>
'''λιποθυμῶ (verbe)''' : s’évanouir.<br>
'''λίπος, -ους (nom commun) (n)''' : graisse ; gras.<br>
'''λιπόψυχος, -ος, -ον (adjectif)''' : pusillanime.<br>
'''λιτότης, -τος (nom commun) (f)''' : simplicité.<br>
'''λιτός, -ή, -όν (adjectif)''' : Uni. Simple, sans apprêts. (Par extension) Pauvre, chétif, faible, petit.<br>
'''λίψ, -ϐος (nom commun) (m)''' : sud-ouest.<br>
'''λογίζομαι (verbe)''' : .<br>
'''λογικός, -ή, -όν (adjectif)''' : intellectuel ; rationnel.<br>
'''λογικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''λογικός''.<br>
'''λογικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''λογικός''.<br>
'''λογικότατα, -, - (adverbe)''' : Superlatif de ''λογικῶς''.<br>
'''λογικότερον, -, - (adverbe)''' : Comparatif de ''λογικῶς''.<br>
'''λογικῶς (adverbe)''' : intellectuellement ; rationnellement.<br>
'''λόγιον, -ίου (nom commun) (n)''' : oracle.<br>
'''λόγος, -ου (nom commun) (m)''' : discours, sujet ; parole.<br>
'''λοιμός, -οῦ (nom commun) (m)''' : peste.<br>
'''λοίμωξις, -ώξεως (nom commun) (f)''' : infestation.<br>
'''λοπαδοτεμαχοσελαχογαλεοκρανιολειψανοδριμυποτριμματοσιλφιοκαραϐομελιτοκατακεχυμενοκιχλεπικοσσυφοφαττοπεριστεραλεκτρυονοπτεκεφαλλιοκιγκλοπελειολαγῳ-οσιραιοϐαφητραγανοπτερυγών (nom commun) (m)''' : bigornocabillofricandortolangoustabricobouillabopoulaupococovin (Plat formé de dix-sept ingrédients différents, décrit par Aristophane dans ''L’Assemblée des femmes''. (v. 1169-1174)<br>
'''λόγχη, -ης (nom commun) (f)''' : lance.<br>
'''λοπάς, -δος (nom commun) (f)''' : écuelle.<br>
'''λορδός, -ή, -όν (adjectif)''' : courbé.<br>
'''λορδότατος, -έρα, -ότερον (adjectif)''' : Comparatif de ''λορδός''.<br>
'''λορδότερος, -άτη, -ότατον (adjectif)''' : Superlatif de ''λορδός''.<br>
'''λορδῶς (adverbe)''' : .<br>
'''λουτήρ, -ῆρος (nom commun) (m)''' : baignoire.<br>
'''λούω (verbe)''' : baigner.<br>
'''λόφος, -ου (nom commun) (m)''' : colline.<br>
'''λοχαγός, -οῦ (nom commun) (m)''' : capitaine.<br>
'''λόχος, -ου (nom commun) (m)''' : compagnie.<br>
'''λύγη, -ης (nom commun) (f)''' : crépuscule, pénombre.<br>
'''λύγξ, -γός (nom commun) (f)''' : hoquet.<br>
'''λύγξ, -κός (nom commun) (m/f)''' : lynx.<br>
'''λυγρός, -ά, -όν (adjectif)''' : Fâcheux, triste. Malfaisant.<br>
'''λύκαινα, -ίνης (nom commun) (f)''' : louve.<br>
'''λυκάνθρωπος, -ώπου (nom commun) (m)''' : loup-garou.<br>
'''λυκίσκος, -ου (nom commun) (m)''' : chien-loup.<br>
'''λύκος, -ου (nom commun) (m)''' : loup.<br>
'''λύμα, -ας (nom commun) (f)''' : Forme dorienne de ''λύμη''.<br>
'''λῦμα, -ύματος (nom commun) (n)''' : .<br>
'''λῦμαξ, -ύμακος (nom commun) (m)''' : .<br>
'''λυμαίνομαι (verbe)''' : .<br>
'''λυμαντήρ, -ῆρος (nom commun) (m)''' : destructeur.<br>
'''λυμαντήριος, -α, -ον (adjectif)''' : injurieux, destructeur.<br>
'''λυμαντής, -οῦ (nom commun) (m)''' : coureur.<br>
'''λυμαντικός, -ή, -όν (adjectif)''' : .<br>
'''λυμάντωρ, - (nom commun) (m)''' : .<br>
'''λῦμαρ, -ύμαρος (nom commun) (m)''' : .<br>
'''λύμασις, -εως (nom commun) (f)''' : .<br>
'''λυμάχη, -ης (nom commun) (f)''' : .<br>
'''λύμη, -ης (nom commun) (f)''' : .<br>
'''λύπη, -ης (nom commun) (f)''' : regret.<br>
'''λυποῦμαι (verbe)''' : être affligé.<br>
'''λυπῶ (verbe)''' : regretter.<br>
'''λύρα, -ας (nom commun) (f)''' : lyre. (Par extension) Constellation de la lyre. (Par extension) Poisson du même nom.<br>
'''λύσσα, -ης (nom commun) (f)''' : Rage. (maladie canine) Fureur belliqueuse, frénésie.<br>
'''λύτρωσις, -ώσεως (nom commun) (f)''' : rédemption.<br>
'''λύττα, -ης (nom commun) (f)''' : Forme attique de ''λύσσα''.<br>
'''λύχνος, -ου (nom commun) (m)''' : lampe.<br>
'''λωρίς, -δος (nom commun) (f)''' : bande.<br>
'''λῶρος, -ώρου (nom commun) (m)''' : bande.<br>
'''Λάζαρος, -άρου (nom propre) (m)''' : Lazare.<br>
'''Λαέρτης, -ου (nom propre) (m)''' : Laërte (père d’Ulysse).<br>
'''Λαῖλαψ, -ίλαπου (nom propre) (m)''' : Lélaps.<br>
'''Λάκαινα, -ίνης (nom commun) (m)''' : Laconienne.<br>
'''Λακεδαιμόνιος, -ίου (nom commun) (m)''' : Lacédémonien.<br>
'''Λακεδαίμων, -ονος (nom propre) (f)''' : Lacédémone.<br>
'''Λακωνία, -ας (nom propre) (m)''' : Laconie.<br>
'''Λάκων, -ος (nom commun) (m)''' : Laconien.<br>
'''Λάμια, -ας (nom propre) (m)''' : Lamia.<br>
'''Λαμπάς, -δος (nom propre) (f)''' : Lampade.<br>
'''Λαμπετίη, -ης (nom propre) (f)''' : Lampétie.<br>
'''Λάμπος, -ου (nom propre) (m)''' : Lampos.<br>
'''Λαοδίκη, -ης (nom propre) (f)''' : Laodicé.<br>
'''Λαοκόων, -οντος (nom propre) (m)''' : Laocoon.<br>
'''Λαομέδων, -οντος (nom propre) (m)''' : Laomédon (fils d’Ilos et d’Eurydice).<br>
'''Λατῖνος, -ίνου (nom propre) (m)''' : Latinos.<br>
'''Λάτιον, -ίου (nom propre) (n)''' : Latium.<br>
'''Λατώ, -οῦς (nom propre) (f)''' : Forme dorienne de ''Λητώ''.<br>
'''Λαύρειον, -ίου (nom propre) (m)''' : Laurion.<br>
'''Λάχεσις, -εως (nom propre) (f)''' : Lachésis (deuxième Moire).<br>
'''Λέανδρος, -άνδρου (nom propre) (m)''' : Léandre.<br>
'''Λεία, -ας (nom propre) (f)''' : Léa.<br>
'''Λεύκιππος, -ίππου (nom propre) (m)''' : Leucippe.<br>
'''Λευκοτεκία, -ας (nom propre) (f)''' : Variante de ''Λoυκoτοκία''.<br>
'''Λεωνίδας, -ου (nom propre) (m)''' : Léonidas.<br>
'''Λέων, -οντος (nom propre) (m)''' : Lion.<br>
'''Λεωχάρης, -ου (nom propre) (m)''' : Léocharès.<br>
'''Λήθη, -ης (nom propre) (f)''' : Léthé.<br>
'''Λητώ, -οῦς (nom propre) (f)''' : Léto.<br>
'''Λιϐύη, -ης (nom propre) (f)''' : Lybie.<br>
'''Λίϐυς, -ος (nom commun) (m)''' : Libyen.<br>
'''Λουκᾶνος, -άνου (nom commun) (m)''' : Lucanien.<br>
'''Λουκᾶς, -ᾶ (nom propre) (m)''' : Luke ; Lucas.<br>
'''Λουκιανός, -οῦ (nom propre) (m)''' : Lucien.<br>
'''Λoυκoτοκία, -ας (nom propre) (f)''' : Lutèce.<br>
'''Λουκρητία, -ας (nom propre) (f)''' : Lucrèce.<br>
'''Λουκρήτιος, -ίου (nom propre) (m)''' : Lucrèce.<br>
'''Λυγκεύς, -έως (nom propre) (m)''' : Lyncée.<br>
'''Λυκάμϐης, -ου (nom propre) (m)''' : Lycambès.<br>
'''Λυκάων, -ονος (nom propre) (m)''' : Lycaon.<br>
'''Λύκειον, -ίου (nom propre) (m)''' : Lycée. (Gymnase au nord-est d’Athènes où enseigna Aristote.)<br>
'''Λυκομήδης, -ου (nom propre) (m)''' : Lycomède.<br>
'''Λυκόπολις, -όλεως (nom propre) (f)''' : Assiout.<br>
'''Λυκοῦργος, -ύργου (nom propre) (m)''' : Lycurgue.<br>
'''Λύσανδρος, -άνδρου (nom propre) (m)''' : Lysandre.<br>
'''Λυσιδίκη, -ης (nom propre) (f)''' : Lysidice.<br>
'''Λύσις, -δος (nom propre) (m)''' : Lysis. (œuvre de Platon)<br>
'''Λῦσις, -ύσεως (nom propre) (m)''' Lysis. (philosophe grec)<br>
'''Λωΐς, -δος (nom propre) (f)''' Loïs. (Personnage du Nouveau Testament : ''Timothée'', 1-5)<br>
==Μ==
'''-μα, -τος (suffixe)''' : suffixe marquant des noms neutres abstraits.<br>
'''μά (particule)''' : par. (Particule interjective.) (+ nom de divinité a l’accusatif)<br>
'''μαγειρεῖον, -ίρου (nom commun) (n)''' : cuisine (pièce).<br>
'''μαγειρεύω (verbe)''' : cuisiner.<br>
'''μαγείρισσα, -ας (nom commun) (f)''' : cuisinière.<br>
'''μάγειρος, -ίρου (nom commun) (m)''' : cuisinier.<br>
'''μαγικός, -ή, -όν (adjectif)''' : magique.<br>
'''μαγικῶς (adverbe)''' : magiquement.<br>
'''μαγικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''μαγικός''.<br>
'''μαγικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''μαγικός''.<br>
'''μάγος, -ου (nom commun) (m)''' : Enchanteur, magicien ; sorcier.<br>
'''μαδαρός, -ά, -όν (adjectif)''' : moite ; mouillé.<br>
'''μαδάω (verbe)''' : être moite/mouillé.<br>
'''μᾶζα, -άζης (nom commun) (f)''' : masse (tas).<br>
'''μαζός, -οῦ (nom commun) (m)''' : sein.<br>
'''μάθημα, -ήματος (nom commun) (n)''' : leçon.<br>
'''μαθητής, -οῦ (nom commun) (m)''' : disciple.<br>
'''μάθησις, -ήσεως (nom commun) (f)''' : apprentissage.<br>
'''µαῖα, -ίας (nom propre) (f)''' : mère ; sage-femme.<br>
'''μαίευμα, -ύµατος (nom commun) (n)''' : accouchement.<br>
'''μαίευσις, -ύσεως (nom propre) (f)''' : accouchement.<br>
'''μαιευτικός, -ή, -όν (adjectif)''' : maïeutique.<br>
'''μαιεύομαι (verbe)''' : faire accoucher.<br>
'''μαίνομαι (verbe)''' : être fou, délirer ; être enragé.<br>
'''μαίομαι (verbe)''' : .<br>
'''μαιμάω (verbe)''' : .<br>
'''μακάριος, -ία, -άριον (adjectif)''' : béni, bienheureux ; heureux, fortuné.<br>
'''μακρός, -ά, -όν (adjectif)''' : (idée d’espace) long. (idée de durée) long. (idée de quantité) Grand, fort.<br>
'''μακρύς, -ιά, -ύ (adjectif)''' : long.<br>
'''μαλακτήρ, -ῆρος (nom commun) (m)''' : .<br>
'''μαλάκτης, -ου (nom commun) (m)''' : masseur.<br>
'''μάλαξις, -άξεως (nom commun) (f)''' : massage.<br>
'''μαλάσσω (verbe)''' : masser.<br>
'''μᾶλον, -άλου (nom commun) (n)''' : Forme dorienne et éolienne de ''μῆλον''. (Au sens de « pomme ».)<br>
'''μαλλός, -οῦ (nom commun) (m)''' : touffe de laine.<br>
'''μάμμη, -ης (nom commun) (f)''' : maman.<br>
'''μανδύα, -ας (nom commun) (f)''' : cape.<br>
'''μανδύη, -ης (nom commun) (f)''' : Forme ionienne de ''μανδύα''.<br>
'''μανθάνω (verbe)''' : Apprendre ; s’apercevoir de. (Par suite) Comprendre.<br>
'''μανία, -ας (nom commun) (f)''' : Folie, démence ; enthousiasme, transport.<br>
'''μανίη, -ης (nom commun) (f)''' : Forme ionienne de ''μανία''.<br>
'''μᾶνις, -άνιος (nom commun) (f)''' : Forme dorienne de ''μῆνις''.<br>
'''μαντεία, -ας (nom commun) (f)''' : Prédiction, oracle ; divination.<br>
'''μαντείη, -ης (nom commun) (f)''' : Forme homérique de ''μαντεία''.<br>
'''μαντηΐη, -ης (nom commun) (f)''' : Forme ionienne de ''μαντεία''.<br>
'''μάντις, -εως (nom commun) (m) ''' : devin.<br>
'''μάραθον, -άθου (nom commun) (n)''' : fenouil.<br>
'''μαργαρίδης, -ου (nom commun) (m) ''' : Forme ionienne de ''μαργαρίτης''.<br>
'''μαργαρίτα, -ας (nom commun) (f) ''' : marguerite.<br>
'''μαργαρίτης, -ου (nom commun) (m) ''' : perle.<br>
'''μαρμαίρω (verbe)''' : briller.<br>
'''μαρμάρεος, -έα, -άρεον (adjectif) ''' : brillant ; fait de marbre.<br>
'''μαρμάρινος, -η, -ον (adjectif) ''' : marmoréen.<br>
'''μάρμαρος, -άρου (nom commun) (m)''' : marbre.<br>
'''μαρμαρώδης, -ης, -ες (adjectif)''' : marbré.<br>
'''μάρσιπος, -ίπου (nom commun) (m)''' : sac.<br>
'''μαρσίππιον, -ίου (nom commun) (n)''' : petit sac.<br>
'''μαρτυρέω (verbe)''' : .<br>
'''μαρτύρημα, -ήματος (nom commun) (n)''' : témoignage.<br>
'''μαρτυρία, -ας (nom commun) (f)''' : témoignage.<br>
'''μαρτύριον, -ίου (nom commun) (n)''' : Témoignage, preuve. Sanctuaire dédié à un martyr.<br>
'''μάρτυρ, -ος (nom commun) (m/f)''' : Forme éolienne de ''μάρτυς''.<br>
'''μάρτυς, -ρος (nom commun) (m/f)''' : témoin ; témoin de Dieu.<br>
'''μασδός, -οῦ (nom commun) (m)''' : Forme dorienne de ''μαζός''.<br>
'''μασθός, -οῦ (nom commun) (m)''' : Forme tardive de ''μαζός''.<br>
'''μάσσω (verbe)''' : pétrir, masser.<br>
'''μάσταξ, -κος (nom commun) (f)''' : mâchoire.<br>
'''μαστιάω (verbe)''' : Forme homérique de ''μαστίζω''.<br>
'''μαστίγωσις, -ώσεως (nom commun) (f)''' : flagellation.<br>
'''μαστιγῶ (verbe)''' : fouetter.<br>
'''μαστίζω (verbe)''' : flageller.<br>
'''μαστίσδω (verbe)''' : Forme dorienne de ''μαστίζω''.<br>
'''μάστιξ, -ίγος (nom commun) (f)''' : fouet.<br>
'''μαστός, -οῦ (nom commun) (m)''' : mamelle ; sein.<br>
'''μασχάλη, -ης (nom commun) (f)''' : aisselle (cavité placée sous le bras).<br>
'''μάταιος, -ία, -αιον (adjectif)''' : futile, vain ; frivole.<br>
'''ματαιότης, -τος (nom commun) (f)''' : futilité, vanité ; frivolité.<br>
'''μάτημι (verbe)''' : Forme éolienne de ''πατέω''.<br>
'''μάτηρ, -ρός (nom commun) (f)''' : Forme dorienne de ''μήτηρ''.<br>
'''μάττω (verbe)''' : Forme attique de ''μάσσω''.<br>
'''μαυρός, -ός, -όν (adjectif)''' : Forme alternative de ''ἀμαυρός''.<br>
'''μάχαιρα, -ίρας (nom commun) (f)''' : sabre.<br>
'''μαχαιτάς, -δος (nom commun) (m)''' : Forme éolienne de ''μαχητής''.<br>
'''μαχανά, -ᾶς (nom commun) (f)''' : Forme dorienne de ''μηχανή''.<br>
'''μαχατάρ, -ος (nom commun) (m)''' : Forme laconienne de ''μαχητής''.<br>
'''μαχάτας, -ου (nom commun) (m)''' : Forme dorienne de ''μαχητής''.<br>
'''μαχέομαι (verbe)''' : Forme ionienne de ''μάχομαι''.<br>
'''μάχη, -ῆς (nom commun) (f)''' : Combat ; bataille.<br>
'''μαχητής, -οῦ (nom commun) (m)''' : combattant.<br>
'''μάχλος, -ος, -ον (adjectif)''' : libertin, débauché.<br>
'''μάχομαι (verbe)''' : combattre.<br>
'''μᾶχος, -άχου (nom commun) (n)''' : Forme dorienne de ''μῆχος''.<br>
'''μέγαθος, -άθεος (nom commun) (n)''' : Forme ionienne de ''μέγεθος''.<br>
'''μεγαλομανής, -ής, -ές (adjectif)''' : mégalomane.<br>
'''μέγας, -άλη, -α (adjectif)''' : grand.<br>
'''μέγεθος, -έθους (nom commun) (n)''' : Grandeur, hauteur.<br>
'''μεγιστάν, -ᾶνος (nom commun) (m)''' : magnat.<br>
'''μέγιστος, -η, -ον (adjectif)''' : Superlatif de ''μέγας''.<br>
'''μέδος, -ου (nom commun) (m)''' : hydromel.<br>
'''μέδων, -οντος (nom commun) (m)''' : seigneur.<br>
'''μέδω (verbe)''' : Commander, régner.<br>
'''μεθίημι (verbe)''' : .<br>
'''μέθυστος, -ος, -ον (adjectif)''' : ivre.<br>
'''μέθυ, -ος (nom commun) (n)''' : vin.<br>
'''μεθύω (verbe)''' : être ivre.<br>
'''μεῖγμα, -ίγματος (nom commun) (n)''' : mixture.<br>
'''μείζων, -ων, -ον (adjectif)''' : Comparatif de ''μέγας''.<br>
'''μεῖξις, -ίξεως (nom commun) (f)''' : .<br>
'''μεῖλον, -ίλου (nom commun) (n)''' : Forme béotienne de ''μῆλον''. (Au sens de « petit bétail ».)<br>
'''μεῖραξ, -ίρακος (nom commun) (m/f)''' : jeune fille ; jeune garçon.<br>
'''μείρομαι (verbe)''' : attribuer ; prendre.<br>
'''μείωσις, -ώσεως (nom commun) (f)''' : réduction.<br>
'''μειῶ (verbe)''' : réduire.<br>
'''μελάμπυγος, -ος, -ον (adjectif)''' : Qui a les fesses noires.<br>
'''μελάντατος, -άτη, -άντατον (adjectif)''' : Superlatif de ''μέλας''.<br>
'''μελάντερος, -έρα, -άντερον (adjectif)''' : Comparatif de ''μέλας''.<br>
'''μελάνως (adverbe)''' : en noir.<br>
'''μέλας, -αινα, -αν (adjectif)''' : noir ; sombre.<br>
'''μελεαγρίς, -δος (nom commun) (f)''' : pintade.<br>
'''μελετηρός, -ή, -όν (adjectif)''' : studieux.<br>
'''μελετῶ (verbe)''' : étudier.<br>
'''μελία, -ας (nom commun) (f)''' : frêne.<br>
'''μελίζω (verbe)''' : moduler, chanter.<br>
'''μελίη, -ης (nom commun) (f)''' : Forme ionienne de ''μελία''.<br>
'''μέλι, -τος (nom commun) (f)''' : miel.<br>
'''μέλισσα, -ης (nom commun) (f)''' : abeille.<br>
'''μέλος, -ους (nom commun) (n)''' : Membre, articulation. Chant. <br>
'''μελῳδία, -ας (nom commun) (f)''' : mélodie.<br>
'''μελῳδικός, -ή, -όν (adjectif)''' : mélodieux.<br>
'''μελῳδός, -ή, -όν (adjectif)''' : musical.<br>
'''μέλω (verbe)''' : être un objet de soin ; prendre soin de ; (impersonnel) être un sujet de souci.<br>
'''μέλλων, -οντος (nom commun) (m)''' : futur.<br>
'''μέμψις, -εως (nom commun) (f)''' : blâme ; censure.<br>
'''μέμφομαι (verbe)''' : reprocher.<br>
'''μέν (particule)''' : .<br>
'''μένος, -ους (nom commun) (n)''' : Esprit. Courage, cœur, ardeur, volonté. Désir. Force, violence.<br>
'''μέντοι (particule)''' : cependant.<br>
'''μένω (verbe)''' : rester, demeurer. Attendre<br>
'''μερίς, -δος (nom commun) (f)''' : part.<br>
'''μέρος, -ους (nom commun) (n)''' : partie.<br>
'''μεσαιπόλιος -ος, -ον (adjectif)''' : .<br>
'''µεσοστιχίς, -δος (nom commun) (f)''' : mésostiche.<br>
'''μέσος, -η, -ον (adjectif)''' : médian.<br>
'''μέσος, -ου (nom commun) (m)''' : majeur.<br>
'''μεστός, -ή, -όν (adjectif)''' : plein.<br>
'''μεστῶς (adverbe)''' : pleinement.<br>
'''μεστώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''μεστός''.<br>
'''μεστώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''μεστός''.<br>
'''μεστῶ (verbe)''' : être plein.<br>
'''μέσως (adverbe)''' : au milieu de.<br>
'''μεσώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''μέσος''.<br>
'''μεσώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''μέσος''.<br>
'''μεσηγύ (adverbe)''' : au milieu.<br>
'''μετά (adverbe)''' (Devient ''μετ’'' devant un mot commençant par une voyelle à esprit doux, et ''μεθ’'' devant un mot commençant par une voyelle à esprit rude.) : Au milieu, parmi.<br>
'''μετα- (préfixe)''' : .<br>
'''μεταϐαίνω (verbe)''' : changer ; dépasser.<br>
'''μετάϐασις, -άσεως (nom commun) (f)''' : changement ; dépassement.<br>
'''μεταϐίϐασις, -άσεως (nom commun) (f)''' : transfert ; transmission.<br>
'''μεταϐιϐάζω (verbe)''' : transférer ; transmettre.<br>
'''μεταγιγνώσκω (verbe)''' : regretter.<br>
'''μετακινῶ (verbe)''' : .<br>
'''μεταμέλεια, -ας (nom commun) (f)''' : repentir.<br>
'''μεταμέλω (verbe)''' : se repentir.<br>
'''μεταμόρφωσις, -ώσεως (nom commun) (f)''' : transformation.<br>
'''μετάνοια, -ίας (nom commun) (f)''' : repentance ; pénitence.<br>
'''μετανοῶ (verbe)''' : faire pénitence.<br>
'''μέταξα, -άξης (nom commun) (f)''' : soie.<br>
'''µεταξύ (préposition)''' : entre.<br>
'''μεταφέρω (verbe)''' : transporter.<br>
'''μεταφορά, -άς (nom commun) (f)''' : transport.<br>
'''μεταφράζω (verbe)''' : traduire.<br>
'''μετάφρασις, -άσεως (nom commun) (f)''' : traduction.<br>
'''μετέωρος, -ος, -ον (adjectif)''' : élevé.<br>
'''μέτοικος, -ίκου (nom commun) (m/f)''' : Individu étranger à une cité.<br>
'''μετεωρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''μετέωρος''.<br>
'''μετεωρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''μετέωρος''.<br>
'''μετεωρότατα (adverbe)''' : Superlatif de ''μετεώρως''.<br>
'''μετεωρότερον (adverbe)''' : Comparatif de ''μετεώρως''.<br>
'''μετεώρως (adverbe)''' : de façon élevée.<br>
'''μέτρον, -ου (nom commun) (n)''' : mesure.<br>
'''μέτωπον, -ώπου (nom commun) (n)''' : front.<br>
'''μέχρι (conjonction)''' : jusqu'à ce que.<br>
'''μηδέν (adjectif numéral)''' : zéro.<br>
'''μηδέποτε (adverbe)''' : jamais.<br>
'''μηδικός, -ή, -όν (adjectif)''' : mède.<br>
'''μῆδος, -ήδους (nom commun) (n)''' : mesure.<br>
'''μή (particule)''' : non (subjectif).<br>
'''μηλίτης, -ου (nom commun) (m)''' : cidre.<br>
'''μῆλον, -ήλου (nom commun) (n)''' : Petit bétail ; pomme.<br>
'''μηνιαῖος, -ία -ῖον (suffixe)''' : mensuel.<br>
'''μηναιότατος, -άτη, -ότατον (suffixe)''' : Forme superlative de ''μηνιαῖος''.<br>
'''μηναιότερος, -έρα, -ότερον (suffixe)''' : Forme comapartive de ''μηνιαῖος''.<br>
'''μηναίως (suffixe)''' : mensuellement.<br>
'''μήνη, -ης (nom commun) (f)''' : mère.<br>
'''μῆνιγξ, -ήνιγγος (nom commun) (f)''' : membrane.<br>
'''μῆνις, -ήνιος (nom commun) (f)''' : colère.<br>
'''μήν, -ός (nom commun) (m)''' : mois.<br>
'''μηριαῖος, -ία, -ῖον (adjectif)''' : fémural.<br>
'''μηρóς, -οῦ (nom commun) (m)''' : côté, flanc ; cuisse, fémur.<br>
'''μήτηρ, -ρός (nom commun) (f)''' : mère.<br>
'''μητιάω (verbe)''' : ruse.<br>
'''μητιόεις, -εσσα, -εν (adjectif)''' : .<br>
'''μῆτις, -ήτεως (nom commun) (f)''' : ruse.<br>
'''μήτις, -ις, -ι (pronom)''' : personne.<br>
'''μήτρα, -ας (nom commun) (f)''' : matrice ; ventre ou sein de la mère.<br>
'''μήτρη, -ης (nom commun) (f)''' : Forme ionienne de ''μήτρα''.<br>
'''μητροκτονία, -ας (nom commun) (m)''' : matricide.<br>
'''μητροκτόνος, -ου (nom commun) (m)''' : matricide.<br>
'''μητρυιά, -ᾶς (nom commun) (f)''' : belle-mère.<br>
'''μητρυιός, -οῦ (nom commun) (m)''' : beau-père.<br>
'''μηχανή, -ῆς (nom commun) (f)''' : machine ; engin. Invention.<br>
'''μηχανικός, -ή, -όν (f)''' : mécanique ; ingénieux.<br>
'''μηχανικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''μηχανικός''.<br>
'''μηχανικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''μηχανικός''.<br>
'''μηχανικῶς (adverbe)''' : mécaniquement ; ingénieusement.<br>
'''μῆχος, -ήχους (nom commun) (n)''' : .<br>
'''μιαίνω (verbe)''' : Salir, souiller, polluer, profaner, teindre, colorer.<br>
'''μιαρός, -ά, -όν (adjectif)''' : impur.<br>
'''μίασμα, -άσματος (nom commun) (n)''' : Souillure provenant d’un meurtre. Personne souillée d’un meurtre.<br>
'''μιασμός, -οῦ (nom commun) (m)''' : Action de salir, souiller, tacher ; deuil, vêtements salis en signe de deuil, teinture.<br>
'''μίγδην (adverbe)''' : confusément.<br>
'''μῖγμα, -ίγματος (nom commun) (n)''' : .<br>
'''μικρός, -ά, -όν (adjectif)''' : petit.<br>
'''μικρός, -οῦ (nom commun) (m)''' : auriculaire.<br>
'''μικρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''μικρός''.<br>
'''μικρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''μικρός''.<br>
'''μικρῶς (adverbe)''' : .<br>
'''μιμάς, -δος (nom commun) (f)''' : actrice.<br>
'''μιμεῖσθαι (verbe)''' : imiter.<br>
'''μίμημα, -ήματος (nom commun) (n)''' : Imitation, copie ; contrefaçon.<br>
'''μίμησις, -ήσεως (nom commun) (f)''' : Imitation, représentation.<br>
'''μιμητικός, -ή, -όν (adjectif)''' : représentatif.<br>
'''μιμητικῶς (adverbe)''' : représentativement.<br>
'''μιμητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''μιμητικός''.<br>
'''μιμητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''μιμητικός''.<br>
'''μιμητικώτατα, -, - (adverbe)''' : Superlatif de ''μιμητικῶς''.<br>
'''μιμητικώτερον, -, - (adverbe)''' : Comparatif de ''μιμητικῶς''.<br>
'''μιμναίσκω (verbe)''' : Forme éolienne de ''μιμνήσκω''.<br>
'''μιμνήσκω (verbe)''' : Rappeler à la mémoire. Rappeler une chose, mentionner.<br>
'''μῖμος, -ίμου (nom commun) (m)''' : acteur.<br>
'''μιμοῦμαι (verbe)''' : mimer.<br>
'''μίνθη, -ης (nom commun) (f)''' : menthe.<br>
'''µινώταυρος, -ύρου (nom commun) (m)''' : minotaure.<br>
'''μισανδρία, -ας (nom commun) (f)''' : misandrie.<br>
'''μισάνδρος, -ος, -ον (adjectif)''' : misandre.<br>
'''μισανθρωπία, -ας (nom commun) (f)''' : misanthropie.<br>
'''μισάνθρωπος, -ος, -ον (adjectif)''' : misanthropie.<br>
'''μισθός, -οῦ (nom commun) (m)''' : salaire.<br>
'''μισθοφόρος, -ου (nom commun) (m)''' : mercenaire.<br>
'''μισογύνης, -ου (nom commun) (m)''' : matcho.<br>
'''μισογυνία, -ας (nom commun) (f)''' : misogynie.<br>
'''μῖσος, -ίσους (nom commun) (n)''' : Haine, aversion ; objet de haine.<br>
'''μισῶ (verbe)''' : détester, haïr.<br>
'''μίτος, -ου (nom commun) (m)''' : fil.<br>
'''μίτρα, -ας (nom commun) (f)''' : Ceinture, bandeau ; chapelet de victoire. Guirlande.<br>
'''μίτρη, -ης (nom commun) (f)''' : Forme ionienne de ''μίτρα''.<br>
'''μίτωσις, -ώσεως (nom commun) (f)''' : mitose.<br>
'''μνᾶ, -ᾶς (nom commun) (f)''' : mine (monnaie).<br>
'''μνᾶμα, -άματος (nom commun) (n)''' : Forme dorienne de ''μνῆμα''.<br>
'''μνάμων, -ων, -ον (adjectif)''' : Forme dorienne de ''μνήμων''.<br>
'''μνάομαι (verbe)''' : .<br>
'''μναστήρ, -ῆρος (nom commun) (m)''' : Forme dorienne de ''μνηστήρ''.<br>
'''μνᾶστις, -άστεως (nom commun) (f)''' : Forme dorienne de ''μνῆστις''.<br>
'''μνῆμα, -ήματος (nom commun) (n)''' : tombe.<br>
'''μνημεῖον, -ίου (nom commun) (n)''' : monument.<br>
'''μνημήιον, -ίου (nom commun) (n)''' : Forme dorienne de ''μνημεῖον''.<br>
'''μνήμων, -ων, -ον (adjectif)''' : .<br>
'''μνηστήρ, -ῆρος (nom commun) (m)''' : prétendant.<br>
'''μνῆστις, -ήστεως (nom commun) (f)''' : .<br>
'''µvοῦς, -οῦ (nom commun) (m)''' : chatte (sexe féminin).<br>
'''μόγις (adverbe)'''' : à peine.<br>
'''μόγος, -ου (nom commun) (m)''' : peine ; labeur.<br>
'''μόλις (adverbe)''' : Variante de ''μόγις''.<br>
'''µοῖρα, -ίρας (nom commun) (f)''' : part ; portion. Parti politique.<br>
'''μοιχαλίς, -δος (nom commun) (f)''' : adultère (celle qui la commet).<br>
'''μοιχεία, -ας (nom commun) (f)''' : adultère.<br>
'''μοιχεύω (verbe)''' : commettre l’adultère.<br>
'''μοιχός, -οῦ (nom commun) (m)''' : adultère (celui qui la commet).<br>
'''μόλυϐδος, -ύϐδου (nom commun) (m)''' : plomb.<br>
'''μόλυνσις, -ύνσεως (nom commun) (f)''' : infection.<br>
'''μομφή, -ῆς (nom commun) (f)''' : reproche.<br>
'''μονάς, -δος (nom commun) (f)''' : unité.<br>
'''μοναχεῖον, -ίου (nom commun) (n)''' : monastère.<br>
'''μοναχός, -οῦ (nom commun) (m)''' : moine.<br>
'''μονή, -ῆς (nom commun) (f)''' : action de s’arrêter ; lieu où l’on séjourne.<br>
'''μονόκερως, -έρωτος (nom commun) (f)''' : licorne.<br>
'''μονομαχεῖον, -ίου (nom commun) (n)''' : lieu de duel.<br>
'''μονομαχέω (verbe)''' : se battre en duel.<br>
'''μονομαχία, -ας (nom commun) (f)''' : duel.<br>
'''μονομάχος, -ου (nom commun) (m)''' : duelliste.<br>
'''μονόφθογγος, -όγγου (nom commun) (f)''' : monophtongue.<br>
'''μόνος, -η, -ον (adjectif)''' : Seul, unique ; solitaire.<br>
'''μόνϝος, -η, -ον (adjectif)''' : Forme ancienne de ''μόνος''.<br>
'''μονῳδία, -ας (nom commun) (f)''' : .<br>
'''μόνως (adverbe)''' : uniquement.<br>
'''μονώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''μόνος''.<br>
'''μονώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''μόνος''.<br>
'''μορμολύκα, -ας (nom commun) (f)''' : Forme dorienne de ''μορμολύκη''.<br>
'''μορμολυκεῖον, -ίου (nom commun) (n)''' : croque-mitaine.<br>
'''μορμολύκη, -ης (nom commun) (f)''' : .<br>
'''μορμωτός, -ή, -όν (adjectif)''' : effroyable.<br>
'''μορμῶ -οῦς (nom commun) (m)''' : monstre marin.<br>
'''μορφή, -ῆς (nom commun) (f)''' : forme.<br>
'''μόρφωσις, -ώσεως (nom commun) (f)''' : formation.<br>
'''μορφῶ (verbe)''' : former.<br>
'''μόσχευμα, -ύματος (nom commun) (n)''' : plant.<br>
'''μοσχεύω (verbe)''' : planter.<br>
'''μοσχοκαρύδιον, -ίου (nom commun) (n)''' : Diminutif de ''μοσχοκάρυον''.<br>
'''μοσχοκάρυον, -ύου (nom commun) (n)''' : noix de muscade.<br>
'''μόσχος, -ου (nom commun) (m)''' : Musc ; pousse.<br>
'''μοχθηρός, -ά, -όν (adjectif)''' : malveillant ; souffrant.<br>
'''μοχθηρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''μοχθηρός''.<br>
'''μοχθηρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''μοχθηρός''.<br>
'''μοχθηρῶς, -ά, -όν (adverbe)''' : .<br>
'''μόχθος, -ου (nom commun) (m)''' : peine ; labeur.<br>
'''μοχθῶ (verbe)''' : être malveillant.<br>
'''μουνάς, -δος (nom commun) (f)''' : Forme ionienne de ''μονάς''.<br>
'''μοῦνος, -ύνη, -ῦνον (adjectif)''' : Forme homérique et ionienne de ''μόνος''.<br>
'''μῶλυ, -ώλυος (nom commun) (n)''' : moly.<br>
'''μῶνος, -η, -ον (adjectif)''' : Forme dorienne de ''μόνος''.<br>
'''μορμολύκειον, -ίου (nom commun) (n)''' : masque.<br>
'''μορφή, -ῆς (nom commun) (f)''' : forme.<br>
'''μόρφωσις, -ώσεως (nom commun) (f)''' : formation.<br>
'''μουσεῖον, -ίου (nom commun) (n)''' : musée.<br>
'''μούσμων, -ονος (nom commun) (m)''' : mouflon.<br>
'''μυδάω (verbe)''' : être moite.<br>
'''μυγαλῆ, -ς (nom commun) (f)''' : musaraigne.<br>
'''μύγις (adverbe)''' : Forme éolienne de ''μόγις''.<br>
'''μυελός, -οῦ (nom commun) (m)''' : moelle.<br>
'''μυζῶ (verbe)''' : sucer.<br>
'''μυθικός, -ή, -όν (adjectif)''' : mythique.<br>
'''μυθιστορία, -ας (nom commun) (f)''' : légende.<br>
'''μυθιστορικός, -ή, -όν (adjectif)''' : légendaire.<br>
'''μῦθος, -ύθου (nom commun) (m)''' : mythe.<br>
'''μυῖα, -ίας (nom commun) (f)''' : mouche.<br>
'''μύκης, -τος (nom commun) (m)''' : Champignon. (Par analogie) Toute excroissance fougueuse. Champignon que se forme à la mèche d’une lampe. Virole d’une gaine. Membre viril.<br>
'''μυκτήρ, -ῆρος (nom commun) (m)''' : museau.<br>
'''μύλη, -ης (nom commun) (f)''' : moulin.<br>
'''μυλεργάτης, -ου (nom commun) (m)''' : meunier.<br>
'''μυλών, -ῶνος (nom commun) (m)''' : meunier.<br>
'''μύξα, -ας (nom commun) (f)''' : morve.<br>
'''μυρίαπους, -δος (nom commun) (m)''' : mille-pattes.<br>
'''μυριάς, -δος (nom commun) (f)''' : .<br>
'''μυρίος, -α, -ον (adjectif)''' : Innombrable, immense ; infini.<br>
'''μύρμηξ, -κος (nom commun) (m)''' : fourmi.<br>
'''μυροϐάλανος, -άνου (nom commun) (f)''' : myrobolan.<br>
'''μύρος, -ου (nom commun) (m)''' : parfum.<br>
'''μυρσίνα, - (nom commun) (f)''' : myrtille.<br>
'''μύρτος, -ου (nom commun) (f)''' : myrtille.<br>
'''μῦς, -ός (nom commun) (m)''' : Rat ; souris. Muscle.<br>
'''μύσταξ, -κος (nom commun) (f)''' : Forme dorienne de ''μάσταξ'' ; moustache.<br>
'''μυστήριον, -ίου (nom commun) (n)''' : sacrement.<br>
'''μυστηριώδης, -ης, -ες (adjectif)''' : relatif aux sacrements.<br>
'''μυστικός, -ή, -όν (adjectif)''' : secret.<br>
'''μυστικῶς (adverbe)''' : secrètement.<br>
'''μυστικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''μυστικός''.<br>
'''μυστικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''μυστικός''.<br>
'''µῦ (nom commun) (n)''' : mu.<br>
'''μύχιος, -α, -ον (adjectif)''' : intime.<br>
'''μυχός, -οῦ (nom commun)''' : cœur.<br>
'''μῶλυ, -ώλυος (nom commun) (n)''' : moly.<br>
'''μῶμαι (verbe)''' : méditer.<br>
'''μῶμος, -ώμου (nom commun) (m)''' : blâme.<br>
'''μῶνυξ, -ώνυχος (nom commun) (m)''' : .<br>
'''μωρός, -ά, -όν (adjectif)''' : Émoussé, hébété. Fade, Insipide. Sot, fou, insensé.<br>
'''μῶρος, -ώρα, -ῶρον (adjectif)''' : Forme attique de ''μωρός''.<br>
'''Μάατ (nom propre) (f)''' : Maât.<br>
'''Μαγδαλά, -ᾶς (nom propre) (f)''' : Magdala.<br>
'''Μαγδαληνή, -ῆς (nom propre) (f)''' : Madeleine.<br>
'''Μαθθαῖος, -ίου (nom propre) (m)''' : Forme alternative de ''Ματθαῖος''.<br>
'''Μαθουσάλα, -ας (nom propre) (m)''' : Mathusalem.<br>
'''Μαῖα, -ίας (nom propre) (f)''' : Maïa.<br>
'''Μαῖρα, -ίρας (nom propre) (f)''' : Maera.<br>
'''Μακάριος, -ίου (nom propre) (m)''' : Macaire.<br>
'''Μακαρία, -ας (nom propre) (f)''' : Macaria.<br>
'''Μάνης, -ου (nom propre) (m)''' : Mani (prophète).<br>
'''Μαντώ, -οῦς (nom propre) (f)''' : Manto.<br>
'''Μάξυος, -ου (nom commun) (m)''' : Berbère.<br>
'''Μαραθών, -ῶνος (nom propre) (m)''' : Marathon.<br>
'''Μαργαρίτα, -ας (nom commun) (f) ''' : Marguerite.<br>
'''Μαρδοχαῖος, -ίου (nom propre) (m)''' : Mardochée.<br>
'''Μάρθα, -ας (nom propre) (f)''' : Martha.<br>
'''Μαρία, -ας (nom propre) (f)''' : Marie.<br>
'''Μασσαλία, -ας (nom propre) (f)''' : Marseille.<br>
'''Μασσαλιώτης, -ου (nom commun) (m)''' : Marseillais.<br>
'''Μασσαλιῶτις, -ώτιδα (nom commun) (f)''' : Marseillaise.<br>
'''Μαῦρος, -ύρου (nom commun) (m)''' : Maure.<br>
'''Μαυσωλεῖον, -ίου (nom propre) (m)''' : Mausolée.<br>
'''Μαύσωλος, -ώλου (nom propre) (m)''' : Mausole.<br>
'''Ματθαῖος, -ίου (nom propre) (m)''' : Matthieu.<br>
'''Μέγαιρα, -ίρας (nom propre) (f)''' : Mégère (une des Érynies).<br>
'''Μέδουσα, -ας (nom propre) (f)''' : Méduse.<br>
'''Μελέαγρος, -άγρου (nom propre) (m)''' : Méléagre.<br>
'''Μελησιγενής, -οῦ (nom propre) (m)''' : Mélésigène.<br>
'''Μέλης (nom propre) (m)''' : Mélès (rivière).<br>
'''Μελέτη, -ης (nom propre) (f)''' : Mélété.<br>
'''Μέλισσα, -ίσσης (nom propre) (f)''' : Mélissa.<br>
'''Μελίτη, -ης (nom propre) (f)''' : Mélite.<br>
'''Μελπομένη, -ης (nom propre) (f)''' : Melpomène.<br>
'''Μελχισέδεκ (nom propre) (m)''' : Melchisédech.<br>
'''Μέμφις, -δος (nom propre) (f)''' : Memphis.<br>
'''Μενίππη, -ης (nom propre) (f)''' : Ménippe.<br>
'''Μένιππος, -ίππου (nom propre) (m)''' : Ménippe.<br>
'''Μεχείρ (nom commun) (m)''' : Méchir.<br>
'''Μήδεια, -ίας (nom propre) (f)''' : Médée.<br>
'''Μηδία, -ας (nom propre) (f)''' : Médie.<br>
'''Μῆδος, -ήδου (nom commun) (m)''' : Mède.<br>
'''Μηλινόη, -ης (nom propre) (f)''' : Mélinoé.<br>
'''Μῆτις, -ήτιδος (nom propre) (f)''' : [[wikt:Métis|Métis]].<br>
'''Μίδας, -ου (nom propre) (m)''' : Midas.<br>
'''Μίθρας, -ου (nom propre) (m)''' : Mithra.<br>
'''Μίθρης, -ου (nom propre) (m)''' : Forme ionienne de ''Μίθρας''.<br>
'''Μιθριδάτης, -ου (nom propre) (m)''' : Mithridate.<br>
'''Μιχαίας, -ου (nom propre) (m)''' : Michée.<br>
'''Μίνως, -ωος (nom propre) (m)''' : Minos.<br>
'''Μίσφρης, -ου (nom propre) (m)''' : Menkhéperrê.<br>
'''Μνεῦις, -ύιδος (nom propre) (m)''' : Mnévis.<br>
'''Μοῖρα, -ίρας (nom propre) (f)''' : Moire.<br>
'''Μοῖσα, -ίσης (nom propre) (f)''' : Forme éolienne de ''Μοῦσα''.<br>
'''Μόνοικος, -ίκου (nom propre) (f)''' : Monaco.<br>
'''Μορμώ, -οῦς (nom propre) (f)''' : Mormo.<br>
'''Μορφεύς, -έως (nom propre) (m)''' : [[wikt:Morphée|Morphée]].<br>
'''Μουάμεδ (nom propre) (m)''' : Mahomet.<br>
'''Μοῦσα, -ύσης (nom propre) (f)''' : Muse.<br>
'''Μοϋσῆς, -έως (nom propre) (m)''' : Forme alternative de ''Μωϋσῆς''.<br>
'''Μυκερίνος, -ου (nom propre) (m)''' : Mykérinos.<br>
'''Μύρμηξ, -ηκος (nom propre) (f)''' : Myrmex.<br>
'''Μῶμος, -ώμου (nom propre) (m)''' : Momos.<br>
'''Μωσῆς, -έως (nom propre) (m)''' : Forme alternative de ''Μωϋσῆς''.<br>
'''Μωϋση, -ης (nom propre) (m)''' : Forme alternative de ''Μωϋσῆς''.<br>
'''Μωϋσῆς, -έως (nom propre) (m)''' : Moïse.<br>
'''Μῶσα, -ώσης (nom propre) (f)''' : Forme dorienne de ''Μοῦσα''.<br>
'''Μῶἁ, -ἁς (nom propre) (f)''' : Forme laconienne de ''Μοῦσα''.<br>
==Ν==
'''ναϝός, -οῦ (nom commun) (m)''' : Forme laconienne de ''ναός''.<br>
'''ναί (particule)''' : oui.<br>
'''ναίχι (adverbe)''' : certes.<br>
'''ναίω (verbe)''' : Habiter. Héberger. Construire ; s’installer quelque part.<br>
'''νᾶνος, -άνου (nom commun) (m)''' : nain.<br>
'''ναός, -οῦ (nom commun) (m)''' : temple.<br>
'''νᾶπυ, -άπυος (nom commun) (f)''' : moutarde (plante).<br>
'''νάρθηξ, -κος (nom commun) (f)''' : férule.<br>
'''ναρθηκοφόρος, -ου (nom commun) (m)''' : licteur.<br>
'''νάρκη, -ης (nom commun) (f)''' : torpeur.<br>
'''νάρκωσις, -όσεως (nom commun) (f)''' : engourdissement.<br>
'''ναρκωτικός, -ή, -όν (adjectif)''' : engourdissant.<br>
'''ναρκῶ (verbe)''' : engourdir.<br>
'''νάσσω (verbe)''' : Presser, fouler. Bourrer, emplir ; encombrer.<br>
'''ναύκληρος, -ήρου (nom commun) (m)''' : .<br>
'''ναῦος, -ύου (nom commun) (m)''' : Forme éolienne de ''ναός''.<br>
'''ναῦς, -εώς (nom commun) (f)''' : navire.<br>
'''ναυτικός, -ή, -όν (adjectif)''' : marin.<br>
'''ναύτης, -ου (nom commun) (m)''' : marin.<br>
'''νάφθα, -ης (nom commun) (f)''' : naphte.<br>
'''νεανιεία, -ας (nom commun) (f)''' : jouvence.<br>
'''νεανίας, -ου (nom commun) (m)''' : jouvenceau.<br>
'''νεᾶνις, -άνιδος (nom commun) (f)''' : jouvencelle.<br>
'''νεανιεύομαι (verbe)''' : .<br>
'''νεανίζω (verbe)''' : .<br>
'''νεηνίης, -ου (nom commun) (m)''' : Forme homérique et ionienne de ''νεανίας''.<br>
'''νεῆνις, -ήνιδος (nom commun) (f)''' : Forme homérique et ionienne de ''νεᾶνις''.<br>
'''νεῖκος, -ίκους (nom commun) (n)''' : Bataille ; combat.<br>
'''νεῖλος, -ίλου (nom commun) (m)''' : rivière de vallée.<br>
'''νεῖος, -ία, -ῖον (adjectif)''' : Forme ionienne de ''νέος''.<br>
'''νεκραγωγός, -οῦ (nom commun) (m)''' : nécragogue.<br>
'''νεκρός, -οῦ (nom commun) (m)''' : mort, cadavre.<br>
'''νεκροψία, -ας (nom commun) (f)''' : nécropsie.<br>
'''νεκυαγωγός, -οῦ (nom commun) (m)''' : nékyagogue.<br>
'''νεκυάμϐατος, -άτου (nom commun) (m)''' : .<br>
'''νεκυδαίμων, -ονος (nom commun) (m)''' : .<br>
'''νεκυηπόλος, - (nom commun) (m)''' : .<br>
'''νέκυια, -ίας (nom commun) (f)''' : descente aux Enfers.<br>
'''νεκυοδαίμων, -ονος (nom commun) (m)''' : .<br>
'''νεκυομαντεῖον, -ίου (nom commun) (n)''' : nékyomantien.<br>
'''νεκυοπομπός, -οῦ (nom commun) (m)''' : nékyopompe.<br>
'''νέκυς, -ος (nom commun) (m)''' : mort.<br>
'''νάννα, -ας (nom commun) (f)''' : tante.<br>
'''νέμος, -ους (nom commun) (n)''' : clairière.<br>
'''νέμω (verbe)''' : Distribuer, partager, attribuer une part, diviser, découper. Attribuer à un troupeau la partie du pâturage où on le mène paître. Habiter. Occuper, détenir. Conduire, gouverner, administrer. Tenir pour, regarder comme (avec double accusatif). (Par suite) Choisir pour, admettre comme.<br>
'''νέννος, -ου (nom commun) (m)''' : oncle.<br>
'''νέομαι (verbe)''' : arriver.<br>
'''νεοσσιά, -ᾶς (nom commun) (f)''' : nid.<br>
'''νεοσσός, -οῦ (nom commun) (m)''' : oisillon, poussin.<br>
'''νέος, -α, -ον (adjectif)''' : Nouveau ; jeune. Neuf.<br>
'''νευρόσπαστον, -ου (nom commun) (n)''' : marionnette.<br>
'''νεῦρον, -ύρου (nom commun) (n)''' : Fibre ; nerf.<br>
'''νευρώδης, -ης, -ες (adjectif)''' : nerveux.<br>
'''νεῦσις, -ύσεως (nom commun) (f)''' : nœud.<br>
'''νεύω (verbe)''' : hocher la tête.<br>
'''νέφος, -ους (nom commun) (n)''' : nuage.<br>
'''νεφρῖτις, -ίτης (nom commun) (f)''' : néphrite (maladie).<br>
'''νεφρός, -οῦ (nom commun) (m)''' : rein.<br>
'''νεώς, -ώ (nom commun) (m)''' : Forme attique de ''ναός''.<br>
'''νέω (verbe)''' : Flotter ; filer la laine.<br>
'''νή (particule)''' : par. (Particule interjective.) (+ nom de divinité a l’accusatif)<br>
'''νῆμα, -ήματος (nom commun) (n)''' : fil.<br>
'''νηματικός, -ή, -όν (adjectif)''' : .<br>
'''νηματώδης, -ης, -ες (adjectif)''' : fibreux.<br>
'''νηός, -οῦ (nom commun) (m)''' : Forme ionienne de ''ναός''.<br>
'''νήπιον, -ίου (nom commun) (n)''' : bébé.<br>
'''νήπιος, -ίου (nom commun) (m)''' : bébé.<br>
'''νήριον, -ίου (nom commun) (n)''' : laurier-rose.<br>
'''νῆσσα, -ήσσης (nom commun) (f)''' : canard.<br>
'''νηστεία, -ας (nom commun) (f)''' : jeûne.<br>
'''νηστεύω (verbe)''' : jeûner.<br>
'''νῆττα, -ήττης (nom commun) (f)''' : Forme attique de ''νῆσσα''.<br>
'''νίζω (verbe)''' : nettoyer.<br>
'''νικέω (verbe)''' : Forme ionienne de ''νικῶ''.<br>
'''νίννη, -ης (nom commun) (f)''' : grand-mère ; belle-mère.<br>
'''νίκη, -ης (nom commun) (f)''' : victoire.<br>
'''νικῶ (verbe)''' : vaincre.<br>
'''νιπτήρ, -ῆρος (nom commun) (m)''' : lavabo.<br>
'''νίπτω (verbe)''' : Variante de ''νίζω''.<br>
'''νίφω (verbe)''' : neiger.<br>
'''νίψ, -φος (nom commun) (f)''' : neige.<br>
'''νόημα, -ήµατος (nom commun) (n)''' : Source de la pensée ; intelligence. Pensée (chose).<br>
'''νόησις, -ήσεως (nom commun) (f)''' : pensée (processus).<br>
'''νοητικός, -ή, -όν (adjectif)''' : Mental ; intellectuel.<br>
'''νοητικῶς (adverbe)''' : Mentalement ; intellectuellement.<br>
'''νοητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''νοητικός''.<br>
'''νοητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''νοητικός''.<br>
'''νοητικώτατα, -, - (adverbe)''' : Superlatif de ''νοητικῶς''.<br>
'''νοητικώτερον, -, - (adverbe)''' : Comparatif de ''νοητικῶς''.<br>
'''νοητός, -ή, -όν (adjectif)''' : pensable.<br>
'''νοητῶς (adverbe)''' : .<br>
'''νοητώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''νοητός''.<br>
'''νοητώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''νοητός''.<br>
'''νομάς, -δος (nom commun) (m/f)''' : nomade.<br>
'''νομοθέτης, -ου (nom commun) (m)''' : législateur.<br>
'''νόμος, -ου (nom commun) (m)''' : Ce qui est attribué en partage. (Par suite) Opinion générale. Maxime. Règle de conduite.<br>
'''νομός, -οῦ (nom commun) (m)''' : Part, portion. (Par extension) Division de territoire. Pâturage, pacage. Herbe, fourrage.<br>
'''νόννος, -ου (nom commun) (m)''' : grand-père ; beau-père.<br>
'''νόος, -ου (nom commun) (m)''' : Faculté de penser.<br>
'''νόσημα, -ήματος (nom commun) (n)''' : infirmité ; fléau.<br>
'''νοσοκομεῖον, -ίου (nom commun) (n)''' : hôpital.<br>
'''νοσοκόμος, -ος, -ν (adjectif)''' : soignant<br>
'''νόσος, -ου (nom commun) (f)''' : maladie.<br>
'''νότιος, -ία, -ιον (adjectif)''' : méridional.<br>
'''νοτιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''νότιως''.<br>
'''νοτιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''νότιως''.<br>
'''νοτιώτατα, -, - (adverbe)''' : Superlatif de ''νότιως''.<br>
'''νοτιώτερον, -, - (adverbe)''' : Comparatif de ''νότιως''.<br>
'''νότιως (adverbe)''' : méridionalement.<br>
'''νότος, -ου (nom commun) (m)''' : sud.<br>
'''νουθετέω (verbe)''' : Admonester ; avertir. Conseiller.<br>
'''vοῦς, -οῦ (nom commun) (m)''' : Variante de ''νόος''.<br>
'''νοῶ (verbe)''' : Penser. Percevoir, noter, observer. Ourdir. Avoir l’intention de. (Linguistique) Signifier, en parlant des mots.<br>
'''νυκτάλωψ, -πία (nom commun) (f)''' : nyctalopie.<br>
'''νυκτερίς, -δος (nom commun) (f)''' : chauve-souris.<br>
'''νύκτερος, -ος, -ον (adjectif)''' : nocturne.<br>
'''νύμφα, -ῦμφας''' : Forme dorienne de ''νύμφη''.<br>
'''νύμφη, -ης (nom commun) (f)''' : nymphe.<br>
'''νυμφόληπτος, -ος, -ον (adjectif)''' : .<br>
'''νυμφιάω (verbe)''' : Être possédé des nymphes ; être en délire.<br>
'''νυμφομανία, -ας (nom commun) (n)''' : nymphomanie.<br>
'''νῦν (adverbe)''' : maintenant.<br>
'''νύξ, -κτός (nom commun) (f)''' : nuit.<br>
'''νυός, -οῦ (nom commun) (f)''' : belle-fille.<br>
'''νῦ (nom commun) (n)''' : nu.<br>
'''νῶμα, -ώματος (nom commun) (n)''' : Forme ionienne de ''νόημα''.<br>
'''νωπός, -ή, -όν (adjectif)''' : frais.<br>
'''νῶσις, -ώσεως (nom commun) (f)''' : Forme ionienne de ''νόησις''.<br>
'''νωτίζω (verbe)''' : tourner le dos.<br>
'''νῶτον, -ώτου (nom commun) (n)''' : (Anatomie) Derrière, dos. (Figuré) Large surface courbe (mer, ciel).<br>
'''νωτοστροφέω (verbe)''' : tourner le dos.<br>
'''νώ (pronom personnel)''' : nous (nous deux).<br>
'''Ναϐαταῖος, -ίου (nom commun) (m)''' : Nabatéen.<br>
'''Ναϐατηνή, -ῆς (nom propre) (f)''' : Nabatea.<br>
'''Ναϊάς, -δος (nom propre) (f)''' : Naïade.<br>
'''Νάρκισσος, -ίσσου (nom propre) (m)''' : Narcisse.<br>
'''Ναυσίθοος, - (nom propre) (m)''' : Nausithoos.<br>
'''Ναυσικάα, -ας (nom propre) (f)''' : Nausicaa.<br>
'''Ναυσίνοoς, - (nom propre) (m)''' : Nausinoos.<br>
'''Νέαιρα, -ίρας (nom propre) (f)''' : Nérée.<br>
'''Νεάπολις, -όλεως (nom propre) (f)''' : Naples.<br>
'''Νεῖλος, -ίλου (nom propre) (m)''' : Nil.<br>
'''Νειλῶτις, -ώτεως (nom propre) (f)''' : Nilotis.<br>
'''Νέμεσις, -έσεως (nom propre) (f)''' : [[wikt:Némésis|Némésis]].<br>
'''Νέσσος, -ου (nom propre) (m)''' : Nessos.<br>
'''Νέστωρ, -ορος (nom propre) (m)''' : Nestor.<br>
'''Νεφέλη, -ης (nom propre) (f)''' : Néphélé.<br>
'''Νεφελοκοκκυγία, -ας (nom propre) (f)''' : Coucouville-les-Nuées. (Ville décrite par Aristophane dans ''Les Oiseaux''.)<br>
'''Νηρεύς, -έως (nom propre) (m)''' : Nérée.<br>
'''Νηρῇς, -δος (nom propre) (f)''' : Néréide.<br>
'''Νίκαια, -ίας (nom propre) (f)''' : Nice.<br>
'''Νίκανδρος, -άνδρου (nom propre) (m)''' : Nicandre.<br>
'''Νίκη, -ης (nom propre) (f)''' : [[wikt:Niké|Niké]].<br>
'''Νικόλαος, -άου (nom propre) (m)''' : Nicolas.<br>
'''Νιόϐη, -ης (nom propre) (f)''' : Niobé.<br>
'''Νότος, -ου (nom propre) (m)''' : [[wikt:Notos|Notos]]. (dieu du vent du sud)<br>
'''Νουμιδία, -ας (nom propre) (f)''' : Numidie.<br>
'''Νύξ, -κτός (nom propre) (f)''' : [[wikt:Nyx|Nyx]].<br>
'''Νῶε (nom propre) (m)''' : Noé.<br>
==Ξ==
'''ξανθóς, -ή, -όν (adjectif)''' : Jaune ; jaunâtre. Blond.<br>
'''ξενοδοχεῖον, -ίου (nom commun) (n)''' : hôtel.<br>
'''ξένος, -η, -ον (adjectif)''' : étranger, insolite.<br>
'''ξένος, -ου (nom commun) (m)''' : étranger ; hôte.<br>
'''ξενών, -ῶνος (nom commun) (f)''' : .<br>
'''ξέσις, -εως (nom commun) (f)''' : .<br>
'''ξεχνῶ (verbe)''' : oublier.<br>
'''ξέω (verbe)''' : gratter ; raser. Polir.<br>
'''ξηρός, -ά, -όν (adjectif)''' : sec.<br>
'''ξῖ (nom commun) (n)''' : xi.<br>
'''ξιφίας, -ου (nom commun) (m)''' : espadon.<br>
'''ξιφίδιον, -ίου (nom commun) (n)''' : dague.<br>
'''ξίφος, -ους (nom commun) (n)''' : glaive.<br>
'''ξύλον, -ου (nom commun) (n)''' : bois (matériau). Tout objet en bois.<br>
'''ξυλόσπογγος, -όγγου (nom commun) (m)''' : tersorium.<br>
'''ξύλωσις, -ώσεως (nom commun) (f)''' : charpente.<br>
'''ξυνός, -ή, -όν (adjectif)''' : Forme ionienne de ''κοινός''.<br>
'''ξύω (verbe)''' : Variante de ''ξέω''.<br>
'''Ξενοφῶν, -τος (nom propre) (m)''' : Xénophon.<br>
'''Ξέρξης, -ου (nom propre) (m)''' : Xerxès.<br>
'''Ξοῦθος, -ύθου (nom propre) (m)''' : Xouthos.<br>
==Ο==
'''ὁ (article défini)''' : le.<br>
'''ὄασις, -άσεως (nom commun) (f)''' : Voie, route ; chemin.<br>
'''ὀγδοήκοντα (adjectif numéral)''' : quatre-vingts.<br>
'''ὀγκάομαι (verbe)''' : .<br>
'''ὄγκος, -ου (nom commun) (m)''' : tumeur.<br>
'''ὄγχνη, -ης (nom commun) (f)''' : poirier.<br>
'''ὅδερος, -έρου (nom commun) (m)''' : ventre.<br>
'''ὁδηγός, -οῦ (nom commun) (m)''' : conducteur.<br>
'''ὁδηγῶ (verbe)''' : conduire.<br>
'''ὁδός, -οῦ (nom commun) (f)''' : Voie, route ; chemin.<br>
'''ὀδούς, -όντος (nom commun) (m)''' : dent.<br>
'''ὀδμή, -ῆς (nom commun) (f)''' : Forme plus ancienne de ''ὀσμή''.<br>
'''ὀδύνη, -ης (nom commun) (f)''' : douleur ; chagrin.<br>
'''ὀδυσάω (verbe)''' : Haïr ; être fâché.<br>
'''ὀδυσσειακός, -ή, -όν (adjectif)''' : odysséen.<br>
'''ὀδύσσομαι (verbe)''' : Être irrité contre quelqu’un.<br>
'''ὄζω (verbe)''' : Exhaler une odeur.<br>
'''ὅθεν (adverbe relatif)''' : d’où (je viens).<br>
'''οἷ (adverbe relatif)''' : là où (je vais).<br>
'''ὀθόνη, -ης (nom commun) (f)''' : toile.<br>
'''οἴ (interjection)''' : hélas ; ouille.<br>
'''οἴκημα, -ήματος (nom commun) (n)''' : résidence.<br>
'''οἰκηματικός, -ή, -όν (adjectif)''' : résidentiel.<br>
'''οἰκημάτιον, -ίου (nom commun) (n)''' : .<br>
'''οἰκητήριον, -ίου (nom commun) (n)''' : domicile.<br>
'''οἰκοπεδικός, -ή, -όν (adjectif)''' : .<br>
'''οἰκόπεδον, -έδου (nom commun) (n)''' : .<br>
'''οἰκοπεδοποίησις, -ήσεως (nom commun) (f)''' : .<br>
'''οἶκος, -ου (nom commun) (m)''' : maison.<br>
'''οἰκοῦμαι (verbe)''' : .<br>
'''οἰκουμένη, -ης (nom commun) (f)''' : .<br>
'''οἰκουμενικός, -ή, -όν (adjectif)''' : .<br>
'''ϝοῖκος, -ίκου (nom commun) (m)''' : Forme ancienne de ''οἶκος''.<br>
'''οἰκεῖος, -ία, -ῖον (adjectif)''' : de la maison.<br>
'''οἰκός, -τος (nom commun) (n)''' : Forme ionienne de ''εἰκός''.<br>
'''οἰκοτροφεῖον, -ίου (nom commun) (n)''' : pensionnat.<br>
'''οικότροφος, -όφου (nom commun) (m/f)''' : pensionnaire.<br>
'''οἶκτος, -ἴκτου (nom commun) (m)''' : pitié.<br>
'''οἰκτρός, -ά, -όν (adjectif)''' : .<br>
'''οἰκτρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''οἰκτρός''.<br>
'''οἰκτρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''οἰκτρός''.<br>
'''οἰκτρότατα, -, - (adverbe)''' : Superlatif de ''οἰκτρῶς''.<br>
'''οἰκτρότερον, -, - (adverbe)''' : Comparatif de ''οἰκτρῶς''.<br>
'''οἰκτρῶς (adverbe)''' : .<br>
'''οἰκῶ (verbe)''' : Habiter ; occuper.<br>
'''οἴμοι (interjection)''' : pauvre de moi.<br>
'''οἶνος, -ἴνου (nom commun) (m)''' : vin.<br>
'''ϝοῖνος, -ίνου (nom commun) (m)''' : Forme dorienne de ''οἶνος''.<br>
'''οἰνοχόη, -ης (nom commun) (f)''' : œnochoé ; échansonne.<br>
'''οἰνοχόος, -ου (nom commun) (m)''' : échanson.<br>
'''ὄϊς, -ΐος (nom commun) (m/f)''' : bélier ; brebis.<br>
'''οἶς, -ός (nom commun) (m/f)''' : Forme attique de ''ὄϊς''.<br>
'''οἷος, -ἵα, -ἷον (adjectif)''' : tel.<br>
'''οἰσοφάγος, -ου (nom commun) (m)''' : œsophage.<br>
'''οἶσος, -ἴσου (nom commun) (m)''' : gattilier.<br>
'''οἶστρος, -ίστρου (nom commun) (m)''' : (au propre) taon. (au figuré) fureur, désir ; passion.<br>
'''οἶτος, -ἴτου (nom commun) (m)''' : destin.<br>
'''οἰφόλης, -ης, ες (adjectif)''' : obscène.<br>
'''οἴφω (verbe)''' copuler.<br>
'''οἰωνοσκοπία, -ας (nom commun) (f)''' : augure.<br>
'''οἰωνοσκόπος, -ου (nom commun) (m)''' : augure.<br>
'''οἰωνός, -οῦ (nom commun) (m)''' : présage, augure.<br>
'''ὀκνείω (verbe)''' : Forme homérique de ''ὀκνέω''.<br>
'''ὀκνέω (verbe)''' : hésiter.<br>
'''ὄκνος, -ου (nom commun) (m)''' : hésitation.<br>
'''ὀκτώ (adjectif numéral)''' : huit.<br>
'''ὄκχος, -ου (nom commun) (m)''' : Forme poétique de ''ὄχος''.<br>
'''ὀλιγάρχης, -ου (nom commun) (m)''' : oligarque.<br>
'''ὀλιγαρχία, -ας (nom commun) (f)''' : oligarchie.<br>
'''ὀλίγος, -η, -ον (adjectif)''' : .<br>
'''ὁλικός, -ή , -όν (adjectif)''' : total.<br>
'''ὁλικῶς (adverbe)''' : totalement.<br>
'''ὁλικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ὁλικός''.<br>
'''ὁλικώτερος, -έρη, -ώτερον (adjectif)''' : Comparatif de ''ὁλικός''.<br>
'''ὁλικώτατα, -, - (adverbe)''' : Superlatif de ''ὁλικῶς''.<br>
'''ὁλικώτερον, -, - (adverbe)''' : Comparatif de ''ὁλικῶς''.<br>
'''ὄλισϐος, -ίσϐου (nom commun) (m)''' : godemichet.<br>
'''ὁλκή, -ῆς (nom commun) (f)''' : attraction.<br>
'''ὄλλυμι (verbe)''' : (Sens actif) Perdre, causer la perte, détruire, annihiler. Mourir. (Sens passif) Périr.<br>
'''ὅλος, -η, -ον (adjectif)''' : entier, complet.<br>
'''ὁλοφύρομαι (verbe)''' : .<br>
'''ὄλπη, -ης (nom commun) (f)''' : .<br>
'''ὀλυμπιακός, -ή, -όν (adjectif)''' : olympien.<br>
'''ὀλυμπικός, -ή, -όν (adjectif)''' : olympique.<br>
'''ὀλύμπιος, -ία, -ύμπιον (adjectif)''' : olympien.<br>
'''ὁμαδικός, -ήν, -όν (adjectif)''' : .<br>
'''ὁμαλός, -ή, -όν (adjectif)''' : lisse.<br>
'''ὁμάς, -δος (nom commun) (f)''' : équipe.<br>
'''ὄμϐρος, -ου (nom commun) (m)''' : tempête de pluie, orage, envoyé par Zeus. Eau. Inondation.<br>
'''ὁμηρικός, -ή, -όν (adjectif)''' : homérique.<br>
'''ὅμηρος, -ήρου (nom commun) (m/f)''' : otage.<br>
'''ὂ μικρόν (nom commun) (n)''' : omicron.<br>
'''ὁμιλία, -ας (nom commun) (f)''' : discours.<br>
'''ὁμιλῶ (verbe)''' : discourir.<br>
'''ὁμίχλη, -ης (nom commun) (f)''' : brouillard.<br>
'''ὄμμα, -τος (nom commun) (n)''' : œil.<br>
'''ὅμοιος, ία, -ον (adjectif)''' : semblable.<br>
'''ὁμοῖος, -, -ῖον (adjectif)''' : Forme homérique, ionienne, et ancienne attique de ''ὅμοιος''.<br>
'''ὁμοίιος, -, - (adjectif)''' : Forme homérique de ''ὅμοιος''.<br>
'''ὁμοιοκαταληξία, -ας (nom commun) (f)''' : rime.<br>
'''ὁμοιοκατάληκτος, -η, -ο (adjectif)''' : rimique.<br>
'''ὁμοιοκαταληκτῶ (verbe)''' : rimer.<br>
'''ὁμοούσιος, -ος, -ον (adjectif)''' : de même substance.<br>
'''ὁμοιούσιος, -ος, -ον (adjectif)''' : de nature semblable.<br>
'''ὁμοιοτέλευτος, -ύτου (nom commun) (m)''' : homéotéleute.<br>
'''ὀμοίωμα, -ώματος (nom commun) (n)''' : effigie.<br>
'''ὁμόνοια, -ας (nom commun) (f)''' : concorde.<br>
'''ὁμός, -ή, -όν (adjectif)''' : Pareil ; semblable.<br>
'''ὁμοῦ (adverbe)''' : En un même lieu, ensemble. (Par suite) Ensemble, à la fois. (Par extension) Auprès, proche.<br>
'''ὀμφαλός, -οῦ (nom commun) (m)''' : nombril.<br>
'''ὀμφή, -ῆς (nom commun) (f)''' : oracle, prophétie.<br>
'''ὀμφής, -ής, -ές : (adjectif)''' : prophétique.<br>
'''ὀμφητήρ, -τρός (nom commun) (m)''' : prophète.<br>
'''ὄνειδος, -ίδους (nom commun) (n)''' : Reproche ; disgrâce.<br>
'''ὀνειρευτής, -οῦ (nom commun) (m)''' : rêveur.<br>
'''ὄνειρος, -ίρου (nom commun) (m)''' : rêve.<br>
'''ὀνειροπόλος, -ου (nom commun) (m)''' : rêvasseur.<br>
'''ὀνειροπωλῶ (verbe)''' : rêvasser.<br>
'''ὀνοκρόταλος, -άλου (nom commun) (m)''' : pélican.<br>
'''ὄνομα, -όματος (nom commun) (n)''' : nom.<br>
'''ὀνοματοποιία, -ας (nom commun) (f)''' : mot imitant un son.<br>
'''ὄνος, -ου (nom commun) (m/f)''' : Âne ; ânesse.<br>
'''ὄνυξ, -χος (nom commun) (m)''' : ongle.<br>
'''ὄνυμα, -ύματος (nom commun) (n)''' : Forme dorienne et éolienne de ''ὄνομα''.<br>
'''ὀνύχιον, -ίου (nom commun) (m)''' : griffe.<br>
'''ὄνω (adverbe)''' : Forme éolienne de ''ἄνω''.<br>
'''ὀξίνης, -ης, -ες (adjectif)''' : aigre.<br>
'''ὀξύα, -ας (nom commun) (f)''' : Hêtre. Arme en bois de hêtre comme un épieu ou un javelot.<br>
'''ὀξύγαλα, -άλακτος (nom commun) (n)''' : petit-lait.<br>
'''ὀξύμωρος, -ος, -ον (adjectif)''' : oxymore.<br>
'''ὀξυπόριον, -ίου (nom commun) (n)''' : carminatif.<br>
'''ὀξύς, -εῖα, -ύ (adjectif)''' : Aigu. Pointu. Tranchant en parlant d’une arme. Piquant en parlant de la sensation (chaleur, froid, douleur), du goût (acidité). Vif. Fin, pénétrant. Prompt à l’action.<br>
'''ὀξύτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ὀξύς''.<br>
'''ὀξύτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ὀξύς''.<br>
'''ὀπαδός, -οῦ (nom commun) (m)''' : fan.<br>
'''ὀπάζω (verbe)''' : .<br>
'''ὀπή, -ῆς (nom commun) (f)''' : Trou ; orifice.<br>
'''ὄπιον, -ίου (nom commun) (n)''' : opium.<br>
'''ὄπισθεν (adverbe)''' : arrière, derrière.<br>
'''ὄπις, -δος (nom commun) (f)''' : (Sens négatif) Revanche, vengeance. (Sens positif) Regard bienveillant, égard. (Religion) Égards dûs aux dieux par les hommes.<br>
'''ὄπις (adverbe)''' : en arrière (sans mouvement).<br>
'''ὀπίσω (adverbe)''' : en arrière (avec mouvement), vers l’arrière.<br>
'''ὁπλή, -ῆς (nom commun) (f)''' : sabot (corne pédestre de certains animaux).<br>
'''ὅπλη, -ης (nom commun) (f)''' : Forme homérique de ''ὁπλή''.<br>
'''ὁπλήεις, -εις, -ης (f)''' : .<br>
'''ὁπλίτης, -ου (nom commun) (m)''' : Soldat pesamment armé.<br>
'''ὅπλον, -ου (nom commun) (n)''' : Outil. (Au pluriel) Armes.<br>
'''ὀποπάναξ, -κος (nom commun) (m)''' : jus de légume.<br>
'''ὀπός, -οῦ (nom commun) (m)''' : jus ; suc.<br>
'''ὀπτασία, -ας (nom commun) (f)''' : fantasme.<br>
'''ὀπτικός, -ή, -όν (adjectif)''' : visuel.<br>
'''ὀπτός, -ή, -όν (adjectif)''' : Rôti, grillé. (Par extension) Cuit. Visible, vu.<br>
'''ὀπτῶ (verbe)''' : Rôtir, griller.<br>
'''ὅραμα, -άματος (nom commun) (n)''' : vision, spectacle.<br>
'''ὄρασις, -άσεως (nom commun) (f)''' : vue.<br>
'''ὁρατός, -ή, -όν, (adjectif)''' : visible.<br>
'''ὁράω (verbe)''' : voir, regarder.<br>
'''ὀρεάς, -δος (nom commun) (f)''' : oréade.<br>
'''ὁρέω (verbe)''' : Forme ionienne de ''ὁράω''.<br>
'''ὄρημι (verbe)''' : Forme éolienne de ''ὁράω''.<br>
'''ὄργανον, -άνου (nom commun) (n)''' : organe, instrument de musique.<br>
'''ὀργασμός, -οῦ (nom commun) (m)''' : point culminant du plaisir sexuel.<br>
'''ὀργάς, -δος (nom commun) (f)''' : .<br>
'''ὀργή, -ῆς (nom commun) (f)''' : colère.<br>
'''ὀργῶ (verbe)''' : enfler, être empli de désir amoureux.<br>
'''ὄρεξις, -έξεως (nom commun) (f)''' : appétit.<br>
'''ὀρθογραφία, -ας (nom commun) (f)''' : écriture correcte.<br>
'''ὀρθόδοξος, -ος, -ον (adjectif)''' : orthodoxe.<br>
'''ὀρθός, -ή, -όν (adjectif)''' : droit.<br>
'''ὀρθότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ὀρθός''.<br>
'''ὀρθότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ὀρθός''.<br>
'''ὀρθῶς (adverbe)''' : droitement.<br>
'''ὁρίζων, -οντος (m)''' : horizon.<br>
'''ὁρίζω (verbe)''' : limiter, borner.<br>
'''ὁρμά, -ᾶς (nom commun) (f)''' : Saut, essor.<br>
'''ὁρμέω (verbe)''' : (Marine) Mouiller, être à l'ancrage.<br>
'''ὁρμή, -ῆς (nom commun) (f)''' : élan (rapidité).<br>
'''ὅρμημα, -ήματος (nom commun) (f)''' : impulsion, désir.<br>
'''ὅρμησις, -ήσεως (nom commun) (f)''' : élan.<br>
'''ὁρμητικός, -ή, -όν (adjectif)''' : impétueux.<br>
'''ὁρμητικῶς (adverbe)''' : impétueusement.<br>
'''ὁρμητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ὁρμητικός''.<br>
'''ὁρμητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ὁρμητικός''.<br>
'''ὁρμητικώτατα, -, - (adverbe)''' : Superlatif de ''ὁρμητικῶς''.<br>
'''ὁρμητικώτερον, -, - (adverbe)''' : Comparatif de ''ὁρμητικῶς''.<br>
'''ὁρμητικότης, -τος (nom commun) (f)''' : impétuosité.<br>
'''ὁρμῶ (verbe)''' : Pousser, démarrer, mettre en mouvement ; presser, se presser ; aller vite.<br>
'''ὄρνεον, -έου (nom commun) (n)''' : oiseau.<br>
'''ὄρνις, -θος (nom commun) (m/f)''' : oiseau.<br>
'''ὄρνυμι (verbe)''' : animer, inciter.<br>
'''ὄρος, -ους (nom commun) (m)''' : Montagne ; colline, hauteur.<br>
'''ὅρος, -ου (nom commun) (m)''' : borne.<br>
'''ὀρός, -οῦ (nom commun) (m)''' : sérum.<br>
'''ὄρρος, -ου (nom commun) (m)''' : (Familier) cul.<br>
'''ὀροφή, -ῆς (nom commun) (f)''' : plafond.<br>
'''ὁρόω (verbe)''' : Forme homérique de ''ὁράω''.<br>
'''ὄρτυξ, -ύκου (nom commun) (m/f)''' : caille.<br>
'''ὀρυκτήρ, -ῆρος (nom commun) (m)''' : mineur.<br>
'''ὄρυξ, -γος (nom commun) (m)''' : oryx.<br>
'''ὀρύσσω (verbe)''' : creuser.<br>
'''ὀρφανία, -ας (nom commun) (f)''' : .<br>
'''ὀρφανίζω (verbe)''' : .<br>
'''ὀρφανιστής, -οῦ (nom commun) (m)''' : .<br>
'''ὀρφανός, -ή, -όν (adjectif)''' : orphelin.<br>
'''ὀρφανός, -οῦ (nom commun) (m)''' : orphelin.<br>
'''ὀρφανοτροφεῖον, -ίου (nom commun) (n)''' : orphelinat.<br>
'''ὄρανος, -άνου (nom commun) (m)''' : Forme éolienne de ''οὐρανός''.<br>
'''ὀρείχαλκος, -άλκου (nom commun) (m)''' : orichalque.<br>
'''ὄρφνα, -ας (nom commun) (f)''' : Forme dorienne de ''ὄρφνη''.<br>
'''ὄρφνη, -ης (nom commun) (f)''' : obscurité.<br>
'''ὀρφνός, -ή, -όν (adjectif)''' : obscur.<br>
'''ὀρφνότατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ὀρφνός''.<br>
'''ὀρφνότερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ὀρφνός''.<br>
'''ὀρφνῶς (adverbe)''' : obscurément.<br>
'''ὀρχήστρα, -ας (nom commun) (f)''' : place du chœur.<br>
'''ὀρχηστρίς, -δος (nom commun) (f)''' : danseuse.<br>
'''ὄρχις, -εως (nom commun) (m/f)''' : (Au masculin) Testicule, ovaire, orchidée. (Au féminin) Sorte d’olive.<br>
'''ὀρχοῦμαι (verbe)''' : danser.<br>
'''ὄσδω (verbe)''' : Forme dorienne de ''ὄζω''.<br>
'''ὀσμή, -ῆς (nom commun) (f)''' : odeur.<br>
'''ὀστέον, -ου (nom commun) (n)''' : os ; noyau.<br>
'''ὀστοῦν, -οῦ (nom commun) (n)''' : Forme attique de ''ὀστέον''.<br>
'''ὀστεῦν, -εῦ (nom commun) (n)''' : Forme poétique de ''ὀστέον''.<br>
'''ὄστιον, -ίου (nom commun) (n)''' : Forme éolienne de ''ὀστέον''.<br>
'''ὅς, ἥ, ὅ''' : (adjectif démonstratif) Celui-ci, celle-ci, ceci. (pronom relatif) Qui, lequel, laquelle.<br>
'''ὄστρειον, -ίου (nom commun) (n)''' : Forme de ''ὄστρεον''.<br>
'''ὄστρεον, -έου (nom commun) (n)''' : huître.<br>
'''ὀσφραίνομαι (verbe)''' : sentir.<br>
'''ὄσφρησις, -ήσεως (nom commun) (f)''' : odorat.<br>
'''ὀσφυαλγία, -ας (nom commun) (f)''' : lumbago.<br>
'''ὀσφῦς, -ύος (nom commun) (f)''' : flanc.<br>
'''οὐ- (préfixe)''' : préfixe privatif.<br>
'''οὐαί (interjection)''' : ah.<br>
'''οὖας, -τός (nom commun) (n)''' : Forme homérique de ''οὖς''.<br>
'''οὐδείς, -μία, -έν (adjectif indéfini ; pronom indéfini)''' : Aucun, personne.<br>
'''οὐδέποτε (adverbe)''' : jamais.<br>
'''οὐδέτερος, -έρα, -έτερον (adjectif)''' : ni l’un ni l’autre.<br>
'''οὕδωρ, -ατος (nom commun) (n)''' : Forme béotienne de ''ὕδωρ''.<br>
'''οὐκί (adverbe)''' : Forme attique et ionienne de ''οὐ''<br>
'''οὐλή, -ῆς (nom commun) (f)''' : cicatrice.<br>
'''οὔνομα, -όματος (nom commun) (n)''' : Forme homérique et ionienne de ''ὄνομα''.<br>
'''οὖθαρ, -ὔθατος (nom commun) (n)''' : Mamelle. (Anatomie) Pis.<br>
'''οὔποτε (adverbe)''' : jamais.<br>
'''οὐροδοχεῖον, -ίου (nom commun) (n)''' : pot de chambre.<br>
'''οὖρον, -ὔρου (nom commun) (n)''' : urine.<br>
'''οὖρος, -ὔρου (nom commun) (m)''' : gardien.<br>
'''οὗ (adverbe relatif)''' : où (je suis).<br>
'''οὐ (particule)''' (Devient ''οὐκ'' devant un mot commençant par une voyelle à esprit doux, et ''οὐχ'' devant un mot commençant par une voyelle à esprit rude.) : non.<br>
'''οὖν (adverbe)''' : Sans doute, réellement, en effet. Donc, eh bien.<br>
'''οὐρά, -ᾶς (nom commun) (f)''' : queue.<br>
'''οὐράνη, -ης (nom commun) (f)''' : pot de chambre.<br>
'''οὐράνιος, -α, -ον (adjectif)''' : céleste.<br>
'''οὐρανός, -οῦ (nom commun) (m)''' : ciel.<br>
'''οὔρησις, -ήσεως (nom commun) (f)''' : miction.<br>
'''οὐρητρίς, -δος (nom commun) (f)''' : pot de chambre.<br>
'''οὖρον, -ὔρου (nom commun) (n)''' : urine.<br>
'''οὐρῶ (verbe)''' : uriner.<br>
'''οὐσία, -ας (nom commun) (f)''' : (Philosophie) Essence, substance, être. Élément, substance première. Biens, fortune, richesse.<br>
'''οὐσιαστικόν, -οῦ (nom commun) (n)''' : substantif.<br>
'''οὐσίη, -ης (nom commun) (f)''' : Forme ionienne de ''οὐσία''.<br>
'''οὐσιώδης, -ης, -ες (adjectif)''' : essentiel.<br>
'''οὖς, ὠτός (nom commun) (n)''' : oreille.<br>
'''οὔτις, -ις, -ι (pronom)''' : personne.<br>
'''οὐχί (adverbe)''' : Forme attique et homérique de ''οὐ''<br>
'''ὀφείλημα, -ήματος (nom commun) (n)''' : dette.<br>
'''ὀφείλω (verbe)''' : devoir de l'argent.<br>
'''ὀφέλλω (verbe)''' : Forme homérique et de ''ὀφείλω''.<br>
'''ὄφελος, -έλους (nom commun) (n)''' : .<br>
'''ὀφθαλμός, -οῦ (nom commun) (m)''' : œil.<br>
'''ὄφις, -εως (nom commun) (m)''' : serpent.<br>
'''ὀφρῦς, -ύος (nom commun) (f)''' : sourcil.<br>
'''ὀχεύω (verbe)''' copuler.<br>
'''ὄχημα, -ήματος (nom commun) (n)''' : véhicule.<br>
'''ϝόχος, -ου (nom commun) (m)''' : Forme ancienne de ''ὄχος''.<br>
'''ὄχθη, -ης (nom commun) (f)''' : Rive ; berge.<br>
'''ὀχλοκρατία, -ας (nom commun) (f)''' : ochlocratie.<br>
'''ὄχλος, -ου (nom commun) (m)''' : Foule ; multitude. Tumulte d'une foule.<br>
'''ὄχνη, -ης (nom commun) (f)''' : poire.<br>
'''ὄχος, -ου (nom commun) (m)''' : Réceptacle, abri. Tout ce qui sert à transporter, véhicule.<br>
'''-οχος, -όχου (suffixe) (m)''' : .<br>
'''ὀχυρός, -ά, -όν (adjectif)''' : ferme.<br>
'''ὀχυρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ὀχυρός''.<br>
'''ὀχυρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ὀχυρός''.<br>
'''ὀχυρῶς (adverbe)''' : fermement.<br>
'''ὀχύρωμα, -ώματος (nom commun) (n)''' : forteresse.<br>
'''ὀχυρῶ (verbe)''' : fortifier.<br>
'''ὀχῶ (verbe)''' : .<br>
'''ὄψις, -εως (nom commun) (f)''' : vue. (perception visuelle)<br>
'''ὄψον, -ου (nom commun) (n)''' : (Cuisine) Plat cuisiné, mets. Assaisonnement, sauce. Délicatesse. (Attique) Poisson.<br>
'''ὀψοποιός, -οῦ (nom commun) (m)''' : cuisinier.<br>
'''ὀψώνης, -ου (nom commun) (m)''' : .<br>
'''ὀψώνιον, -ίου (nom commun) (n)''' : Provisions, vivres. Salaire.<br>
'''ὀψωνῶ (verbe)''' : .<br>
'''ὄψ, -πός (nom propre) (f)''' : Voix. Parole, discours. Œil. (Par métonymie) Visage.<br>
'''Ὀδρύσης, -ου (nom commun) (m)''' : Odryse.<br>
'''Ὀδύσσεια, -ίας (nom propre) (f)''' : Odyssée.<br>
'''Ὀδυσσεία, -ας (nom propre) (f)''' : Forme alternative de ''Ὀδύσσεια''.<br>
'''Ὀδυσσεύς, -έως (nom propre) (m)''' : Ulysse.<br>
'''Ὀζυμάνδιος, -ίου (nom propre) (m)''' : Ozymandias.<br>
'''Ὄθων, -ου (nom propre) (m)''' : Othon.<br>
'''Οἰδίπους, -οδος (nom propre) (m)''' : Œdipe.<br>
'''Οἰκλῆς, -έους (nom propre) (m)''' : Oïclès.<br>
'''Οἰνεύς, -έως (nom propre) (m)''' : Œnée.<br>
'''Οἰνόμαος, -άου (nom propre) (m)''' : Œnomaos.<br>
'''Οἰταῖος, -ίη, -ῖον (adjectif)''' : œtéen.<br>
'''Οἴτη, -ης (nom propre) (f)''' : Œta.<br>
'''Ὄλθφες, - (nom propre) (m)''' : Olivier.<br>
'''Ὀλυμπιάς, -δος (nom propre) (f)''' : olympiade.<br>
'''Ὀλύμπια, -ίων (nom propre) (n)''' : Jeux olympiques.<br>
'''Ὄλυμπος, -ύμπου (nom propre) (m)''' : Olympe.<br>
'''Ὀλυσσεύς, -έως (nom propre) (m)''' : Forme de ''Ὀδυσσεύς''.<br>
'''Ὀλυττεύς, -έως (nom propre) (m)''' : Forme de ''Ὀδυσσεύς''.<br>
'''Ὅμηρος, -ήρου (nom propre) (m)''' : Homère. (poète grec)<br>
'''Ὄνειρος, -ίρου (nom propre) (m)''' : Oniros.<br>
'''Ὀννῶφρις, -ίδος (nom propre) (m)''' : Onnophris.<br>
'''Ὀνούφριος, -ίου (nom propre) (m)''' : Onuphre.<br>
'''Ὄρανος, -άνου (nom propre) (m)''' : Forme éolienne de ''Οὐρανός''.<br>
'''Ὀρσηίς, -δος (nom propre) (f)''' : Orséis (épouse d'Hellen).<br>
'''Ὄρθρος, -ου (nom propre) (m)''' : Orthros.<br>
'''Ὀρφεύς, -έως (nom propre) (m)''' : Orphée.<br>
'''Ὄσιρις, -ίριδος (nom propre) (m)''' : [[wikt:Osiris|Osiris]].<br>
'''Οὐδαῖος, -ίου (nom propre) (m)''' : Udée.<br>
'''Οὑδυσσεύς, -έως (nom propre) (m)''' : Forme de ''Ὀδυσσεύς''.<br>
'''Οὐλιξεύς, -έως (nom propre) (m)''' : Forme de ''Ὀδυσσεύς''.<br>
'''Οὐλίξης, -ους (nom propre) (m)''' : Forme de ''Ὀδυσσεύς''.<br>
'''Οὐόλκος, -ου (nom commun) (m)''' : Volque.<br>
'''Οὐρανία, -ας (nom propre) (f)''' : Uranie.<br>
'''Οὐρανός, -οῦ (nom propre) (m)''' : [[wikt:Ouranos|Ouranos]].<br>
'''Ὀφηλία, -ας (nom propre) (f)''' : Ophélie.<br>
'''Ὀφιοῦχος, -ύχου (nom propre) (f)''' : Serpentaire.<br>
'''Ὄφις, -εως (nom propre) (m)''' : Serpent.<br>
'''Ὀφόις -πως (nom propre) (m)''' : Oupouaout.<br>
==Π==
'''παγά, -ᾶς (nom commun) (f)''' : Forme dorienne de ''πηγή''.<br>
'''παγίδευσις, -ύσεως (nom commun) (f)''' : .<br>
'''παγιδεύω (verbe)''' : piéger.<br>
'''παγίς, -δος (nom commun) (f)''' : piège.<br>
'''παγκόσμιος, -α, -ον (adjectif)''' : universel.<br>
'''πάγκοσμος, -όσμου (nom commun) (m)''' : univers.<br>
'''πάγος, -ου (nom commun) (m)''' : glace (eau à l'état solide).<br>
'''πάγουρος, -ύρου (nom commun) (m)''' : crabe.<br>
'''πάγχυ (adverbe)''' : totalement.<br>
'''παθοποιός, -ός, -όν (adjectif)''' : Qui afflige le corps ou l’âme.<br>
'''πάθος, -ους (nom commun) (n)''' : Ce que l’on éprouve. Évènement, conjoncture. État de l’âme agitée par diverses circonstances.<br>
'''παιάν, -ᾶνος (nom commun) (m)''' : péan.<br>
'''παίγνιον, -ίου (nom commun) (n)''' : jeu.<br>
'''παιδαγωγός, -οῦ (nom commun) (m)''' : éducateur.<br>
'''παιδάριον, -ίου (nom commun) (n)''' : gamin.<br>
'''παιδεραστής, -οῦ (nom commun) (m)''' : amant de garçon.<br>
'''παιδιά, -ᾶς (nom commun) (f)''' : jeu.<br>
'''παιδίον, -ου (nom commun) (n)''' : petit enfant.<br>
'''παιδοτρίϐης, -ου (nom commun) (m)''' : maître de gymnastique.<br>
'''παίδων παῖδες (nom commun) (m/f)''' : Petits-fils/Petites-filles.<br>
'''παίζω (verbe)''' : jouer.<br>
'''παίκτης, -ου (nom commun) (m)''' : joueur.<br>
'''παῖς, -δός (nom commun) (m/f)''' : enfant ; jeune serviteur.<br>
'''παιωνία, -ας (nom commun) (f)''' : pivoine.<br>
'''παιών, -ος (nom commun) (m)''' : physicien.<br>
'''παλαιός, -ά, -όν (adjectif)''' : ancien.<br>
'''παλιγγενεσία, -ας (nom commun) (f)''' : Renaissance, résurrection. Régénération.<br>
'''παλιγγενεσιακός, -ά, -όν (adjectif)''' : palingénésiaque.<br>
'''πάλιν (adverbe)''' : encore.<br>
'''παλλακή, -ῆς (nom commun) (f)''' : concubine.<br>
'''παλλακίς, -δος (nom commun) (f)''' : concubine.<br>
'''πάλλαξ, -κος (nom commun) (m/f)''' : jeune enfant.<br>
'''πάλληξ, -κος (nom commun) (m/f)''' : Forme ionienne de ''πάλλαξ''.<br>
'''πάλλω (verbe)''' : vibrer.<br>
'''παλμός, -οῦ (nom commun) (m)''' : vibration.<br>
'''παλός, -οῦ (nom commun) (m/f)''' : forme dorienne de ''πηλός''.<br>
'''παμμεγεθέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''παμμεγέθης''.<br>
'''παμμεγεθέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''παμμεγέθης''.<br>
'''παμμεγέθης, -ης, -ες (adjectif)''' : immense.<br>
'''παμμεγέθως (adverbe)''' : immensément.<br>
'''πάμπαν (adverbe)''' : totalement.<br>
'''πάμφλεκτος, -ος, -ον (adjectif)''' : .<br>
'''πανάκεια, -ας (nom commun) (f)''' : remède universel.<br>
'''πάναξ, -κος (nom commun) (m)''' : plante médicinale.<br>
'''πανδοκεῖον, -ίου (nom commun) (n)''' : auberge.<br>
'''πανδοκεύς, -έως (nom commun) (m)''' : aubergiste.<br>
'''παράδοσις, -όσεως (nom commun) (f)''' : transmission ; tradition.<br>
'''πανήγυρις, -ύρεως (nom commun) (f)''' : réunion.<br>
'''πάνθηρ, -ος (nom commun) (m)''' : panthère.<br>
'''πανέλοψ, -πος (nom commun) (m)''' : Forme éolienne et dorienne de ''πηνέλοψ''.<br>
'''πανοῦργος, -ύργα, -ῦργον (adjectif)''' : .<br>
'''παντοπωλεῖον, -ίου (nom commun) (n)''' : épicerie.<br>
'''παντοπώλης, -ου (nom commun) (m)''' : épicier.<br>
'''πάνυ (adverbe)''' : totalement.<br>
'''πάππας, -ου (nom commun) (m)''' : papa.<br>
'''πάππος, -ου (nom commun) (m)''' : papi.<br>
'''πάπυρος, -ύρου (nom commun) (m)''' : papyrus.<br>
'''παρά (adverbe ; préposition)''' : (avec le génitif) D’auprès de, du côté de. (avec le datif) Auprès avec l’idée de toute absence de mouvement. Chez, dans, en. (Avec l’accusatif) Auprès de, vers avec l’idée de mouvement.<br>
'''παρά- (préfixe)''' : Auprès. Vers. Le long de. Contre. En détournant.<br>
'''παραϐάλλω (verbe)''' : s’emparer de.<br>
'''παράδειγμα, -ίγματος (nom commun) (n)''' : modèle.<br>
'''παραδειγματικός, -ή, -όν (adjectif)''' : modèle.<br>
'''παράδεισος, -ίσου (nom commun) (m)''' : parc ; paradis.<br>
'''παραδέχομαι (verbe)''' : admettre.<br>
'''παραδίδωμι (verbe)''' : transmettre ; remettre.<br>
'''παράθυρος, -ου (nom commun) (f)''' : Porte de côté ; porte dérobée.<br>
'''παρακινῶ (verbe)''' : .<br>
'''παρακλαυσίθυρον, -ύρου (nom commun) (n)''' : paraclausithuron.<br>
'''παραλείπω (verbe)''' : omettre.<br>
'''παράλειψις, -ίψεως (nom commun) (f)''' : omission.<br>
'''πάρδαλις, -άλεως (nom commun) (f)''' : léopard.<br>
'''παράλληλος, -η, -ον (adjectif)''' : Porte de côté ; porte dérobée.<br>
'''παραλήρησις, -ήσεως (nom commun) (f)''' : délire.<br>
'''παράμεσος, -ου (nom commun) (m)''' : annulaire.<br>
'''παραμυθέομαι (verbe)''' : exhorter.<br>
'''παραμύθιον, -ίου (nom commun) (m)''' : exhortation.<br>
'''παραποίησις, -ήσεως (nom commun) (f)''' : contrefaçon.<br>
'''παραποιῶ (verbe)''' : contrefaire.<br>
'''παράρτημα, -ήματος (nom commun) (n)''' : annexe.<br>
'''παρασάγγης, -ου (nom commun) (m)''' : parasange. (env. 6 km)<br>
'''παράσιτος, -ος, -ον (adjectif)''' : Qui sert de mets en surplus ; Parasite.<br>
'''παραίσθησις, -ήσεως (nom commun) (f)''' : illusion.<br>
'''παράστασις, -άσεως (nom commun) (f)''' : représentation.<br>
'''παραστράτημα, -ήματος (nom commun) (n)''' : incartade.<br>
'''παρειά, -ᾶς (nom commun) (f)''' : joue.<br>
'''παρελθών, -όντος (nom commun) (m)''' : passé.<br>
'''παρεξήγησις, -ήσεως (nom commun) (f)''' : malentendu.<br>
'''παρεξηγῶ (verbe)''' : méprendre.<br>
'''παρεργάτης, -ου (nom commun) (m)''' : assistant.<br>
'''παρεύνασθαι (verbe)''' : représenter.<br>
'''πάρευνος, -ος, -ον (adjectif)''' : .<br>
'''παρθενεία, -ας (nom commun) (f)''' : virginité .<br>
'''παρθένειος, -α, -ον (adjectif)''' : virginal.<br>
'''παρθενικός, -ή, -όν (adjectif)''' : de vierge.<br>
'''παρθενικῶς (adverbe)''' : virginalement.<br>
'''παρθενικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''παρθενικός''.<br>
'''παρθενικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''παρθενικός''.<br>
'''παρθενικώτατα, -, - (adverbe)''' : Superlatif de ''παρθενικῶς''.<br>
'''παρθενικώτερον, -, - (adverbe)''' : Comparatif de ''παρθενικῶς''.<br>
'''παρθένος, -ος, -ον (adjectif)''' : vierge.<br>
'''παρθενεύω (verbe)''' : devenir jeune fille.<br>
'''παρθενών, -ῶνος (nom commun) (m)''' : parthénon.<br>
'''παρθικός, -ή, -όν (adjectif)''' : parthe.<br>
'''παρθιστί (adverbe)''' : en parthe.<br>
'''παρίσθμιον, -ίου (nom commun) (n)''' : (anatomie) amygdale.<br>
'''παρόρμησις, -ήσεως (nom commun) (f)''' : impulsion.<br>
'''παρορμητικός, -ή, -όν (adjectif)''' : impulsif.<br>
'''παρορμητικῶς (adverbe)''' : impulsivement.<br>
'''παρορμητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''παρορμητικός''.<br>
'''παρορμητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''παρορμητικός''.<br>
'''παρορμητικώτατα, -, - (adverbe)''' : Superlatif de ''παρορμητικῶς''.<br>
'''παρορμητικώτερον, -, - (adverbe)''' : Comparatif de ''παρορμητικῶς''.<br>
'''παρορμῶ (verbe)''' : impulser.<br>
'''παρσένος, -ος, -ον (adjectif)''' : Forme laconienne de ''παρθένος''.<br>
'''παρών, -όντος (nom commun) (m)''' : présent.<br>
'''παρωνυχία, -ας (nom commun) (f)''' : panaris.<br>
'''πᾶς, -σα, -ᾶν (adjectif)''' : (Au singulier) Chaque, chacun. Tout entier, tout. (Au pluriel) Tous.<br>
'''πάσσω (verbe)''' : .<br>
'''παστά, -ᾶς (nom commun) (f)''' : pâte.<br>
'''παστός, -ή, -όν (adjectif)''' : .<br>
'''πάσχω (verbe)''' : Être affecté de telle ou telle façon, telle ou telle affection, sensation ou sentiment.<br>
'''πατήρ, -ρός (nom commun) (m)''' : père.<br>
'''πάτος, -ου (nom commun) (m)''' : sentier.<br>
'''πατριά, -ᾶς (nom commun) (f)''' : tribu, famille.<br>
'''πατριάρχης, -ου (nom commun) (m)''' : patriarche.<br>
'''πατρίς, -δος (nom commun) (f)''' : patrie.<br>
'''πατροφονεύς, -έως (nom commun) (m)''' : patricide.<br>
'''πατροφόνος, -ου (nom commun) (m)''' : patricide.<br>
'''πατῶ (verbe)''' : Manger, absorber. Fouler aux pieds. Fouler le sol.<br>
'''παῦρος -ύρα, -ῦρον (adjectif)''' : En petit nombre. (Par extension) Petit, court.<br>
'''παῦσις, -ύσεως (nom commun) (f)''' : cessation.<br>
'''παύω (verbe)''' : cesser.<br>
'''πάφλασμα, -άσματος (nom commun) (n)''' : .<br>
'''πάχνη, -ης (nom commun) (f)''' : givre.<br>
'''παχύς, -εῖα, -ύ (adjectif)''' : Épais, gras.<br>
'''πεδά (préposition)''' : Forme arcado-chypriote, dorienne et éolienne de ''μετά''.<br>
'''πεδιάς, -δος (nom commun) (f)''' : plaine.<br>
'''πεδίον, -ου (nom commun) (n)''' : champ.<br>
'''πέζα, -ης (nom commun) (f)''' : cheville du pied.<br>
'''πεζός, -ή, -όν (adjectif)''' : piéton.<br>
'''πεζῶς (adverbe)''' : .<br>
'''πεζώτατα, -, - (adverbe)''' : Superlatif de ''πεζῶς''.<br>
'''πεζώτερον, -, - (adverbe)''' : Comparatif de ''πεζῶς''.<br>
'''πεζώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''πεζός''.<br>
'''πεζώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''πεζός''.<br>
'''πείθομαι (verbe)''' : persuader ; suivre.<br>
'''πείθω (verbe)''' : convaincre ; persuader.<br>
'''πεῖνα, -ίνας (nom commun) (f)''' : faim.<br>
'''πεινῶ (verbe)''' : avoir faim.<br>
'''πεῖρα, -ίρας (nom commun) (f)''' : Essai, tentative ; épreuve.<br>
'''πειράζω (verbe)''' : Essayer ; tenter.<br>
'''πειράομαι (verbe)''' : essayer.<br>
'''πειρασμός, -οῦ (nom commun) (m)''' : Épreuve, essai ; expérience. Tentation.<br>
'''πειρατής, -οῦ (nom commun) (m)''' : pirate.<br>
'''πειρῶ (verbe)''' : .<br>
'''πελεκάν, -ᾶνος (nom commun) (m)''' : pélican.<br>
'''πελεκῖνος, -ίνου (nom commun) (m)''' : pélican.<br>
'''πέλεκυς, -έκεως (nom commun) (m)''' : hache.<br>
'''πέλμα, -τος (nom commun) (n)''' : Plante du pied ; tige des pommes et des poires.<br>
'''πελώριος, -α, -ον (adjectif)''' : prodigieux.<br>
'''πελωρίς, -δος (nom commun) (f)''' : moule.<br>
'''πέλωρ, -ος (nom commun) (n)''' : prodige.<br>
'''πέμμα, -τος (nom commun) (n)''' : gâteau.<br>
'''πεμπτουσία, -ας (nom commun) (f)''' : quintessence.<br>
'''πεντάφυλλον, -ύλλου (nom commun) (n)''' : potentille.<br>
'''πέντε (adjectif numéral)''' : cinq.<br>
'''πεντήκοντα (adjectif numéral)''' : cinquante.<br>
'''πενθερά, -ᾶς (nom commun) (f)''' : belle-mère.<br>
'''πενθερός, -οῦ (nom commun) (m)''' : beau-père.<br>
'''πένθος, -ους (nom commun) (n)''' : deuil.<br>
'''πέος, -ους (nom commun) (n)''' : pénis.<br>
'''πέπερι, -τος (nom commun) (n)''' : poivre.<br>
'''πέπλος, -ου (nom commun) (m)''' : péplos.<br>
'''πέπλωμα, -ώματος (nom commun) (n)''' : robe.<br>
'''πεπτός, -ή, -όν (adjectif)''' : cuit.<br>
'''πέπων, -ονος (nom commun) (m)''' : melon.<br>
'''περαίνω (verbe)''' : Finir, conclure.<br>
'''πέρασμα, -άσματος (nom commun) (n)''' : passage.<br>
'''πέρας, -τος (nom commun) (n)''' : Fin, limite.<br>
'''πέρδιξ, -ικος (nom commun) (m/f)''' : perdrix.<br>
'''πέρδομαι (verbe)''' : péter (faire un pet).<br>
'''περιϐόλιον, -ίου (nom commun) (n)''' : .<br>
'''περιδέραιον, -ου (nom commun) (n)''' : collier (bijou).<br>
'''περιδέραιος, -ος, -ου (adjectif)''' : .<br>
'''περίζωμα, -ώματος (nom commun) (n)''' : caleçon.<br>
'''περιήγησις, -ήσεως (nom commun) (f)''' : Voyage ; parcours.<br>
'''περιηγοῦμαι (verbe)''' : Voyager ; parcourir.<br>
'''περιηγητής, -οῦ (nom commun) (m)''' : voyageur.<br>
'''περιλαμϐάνω (verbe)''' : .<br>
'''περίληψις, -ήψεως (nom commun) (f)''' : résumé.<br>
'''περίναιον, -ίου (nom commun) (n)''' : périnée.<br>
'''περιπατητικός, -ή, -όν (adjectif)''' : péripatéticien.<br>
'''περιπατῶ (verbe)''' : Circuler, aller et venir ; se promener (Par extension) (Figuré) Se comporter, se conduire, vivre de telle ou de telle manière.<br>
'''περιπέτεια, -ας (nom commun) (f)''' : aventure.<br>
'''περιπίπτω (verbe)''' : tomber autour, sur.<br>
'''περιποιοῦμαι (verbe)''' : s'occuper de.<br>
'''περιποιῶ (verbe)''' : .<br>
'''περιστερά, -ᾶς (nom commun) (f)''' : pigeon, colombe.<br>
'''περιστερός, -οῦ (nom commun) (m)''' : pigeon, colombe. (Pour un mâle de ces espèces.)<br>
'''περιστερεών, -ῶνος (nom commun) (m)''' : pigeonnier, colombier.<br>
'''περιφρονῶ (verbe)''' : mépriser.<br>
'''περιφρούρησις, -ήσεως (nom commun) (f)''' : protection.<br>
'''περιφρουρῶ (verbe)''' : protéger.<br>
'''περκνός, -ή, -όν (adjectif)''' : noirâtre.<br>
'''περκνότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''περκνός''.<br>
'''περκνότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''περκνός''.<br>
'''περκνότατα, -, - (adverbe)''' : Superlatif de ''περκνῶς''.
'''περκνότερον, -, - (adverbe)''' : Comparatif de ''περκνῶς''.
'''περκνῶς (adverbe)''' : noirâtrement.<br>
'''πέρνημι (verbe)''' : Exploiter, vendre. (Au passif) Être vendu.<br>
'''περόνη, -ης (nom commun) (f)''' : agrafe, clavette.<br>
'''περῶ (verbe)''' : passer.<br>
'''πέσσω (verbe)''' : Digérer ; cuire. Mûrir.<br>
'''πετάννυμι (verbe)''' : .<br>
'''πέτασος, -άσου (nom commun) (m)''' : pétase.<br>
'''πετεεινός, -ή, -όν (adjectif)''' : Forme homérique de ''πετεινός''.<br>
'''πετεινός, -ή, -όν (adjectif)''' : capable de voler.<br>
'''πετεσοũχος, -ύχου (nom commun) (m)''' : petsuchos.<br>
'''πετηνός, -ή, -όν (adjectif)''' : Forme ionienne de ''πετεινός''.<br>
'''πετῶ (verbe)''' : voler (planer dans le ciel).<br>
'''πεύκη, -ης (nom commun) (f)''' : pin.<br>
'''πεύθομαι (verbe)''' : Forme poétique de ''πυνθάνομαι''.<br>
'''πέψις, -εως (nom commun) (f)''' : digestion.<br>
'''πηγή, -ῆς (nom commun) (f)''' : source.<br>
'''πήγνυμι (verbe)''' : .<br>
'''πηδάλιον, -ίου (nom commun) (n)''' : talaria.<br>
'''πήδημα, -ήματος (nom commun) (n)''' : saut.<br>
'''πηδόν, -οῦ (nom commun) (n)''' : .<br>
'''πηδός, -οῦ (nom commun) (f)''' : .<br>
'''πηδῶ (verbe)''' : sauter.<br>
'''πῇ (adverbe interrogatif)''' : par où.<br>
'''πήληξ, -κος (nom commun) (m)''' : .<br>
'''πηλός, -οῦ (nom commun) (m/f)''' : boue.<br>
'''πήμων, -ων, -ον (adjectif)''' : nuisible.<br>
'''πηνέλοψ, -πος (nom commun) (m)''' : canard sauvage.<br>
'''πήνη, -ης (nom commun) (f)''' : bobine.<br>
'''πηνήκη, -ης (nom commun) (f)''' : perruque.<br>
'''πηνηκίζω (verbe)''' : .<br>
'''πηνήκισμα, -ίσματος (nom commun) (n)''' : .<br>
'''πῆνος, -ήνου (nom commun) (m)''' : .<br>
'''πῆξις, -ήξεως (nom commun) (f)''' : .<br>
'''πήρα, -ας (nom commun) (f)''' : besace.<br>
'''πήριξ, -ικος (nom commun) (m/f)''' : Forme crétoise de ''πέρδιξ''.<br>
'''πῆχυς, -ήχεως (nom commun) (m)''' : avant-bras.<br>
'''πιέζω (verbe)''' : Presser ; appuyer.<br>
'''πίεσις, -έσεως (nom commun) (f)''' : pression.<br>
'''πῖ (nom commun) (n)''' : pi.<br>
'''πίθακος, -άκου (nom commun) (m)''' : Forme dorienne de ''πίθηκος''.<br>
'''πιθανός, -ή, -όν (adjectif)''' : probable.<br>
'''πιθανῶς (adverbe)''' : probablement.<br>
'''πιθανώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''πιθανός''.<br>
'''πιθανώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''πιθανός''.<br>
'''πιθηκοειδής, -ής, -ές (adjectif)''' : .<br>
'''πίθηκος, -ήκου (nom commun) (m)''' : singe.<br>
'''πίθων, -ονος (nom commun) (m)''' : Singe, petit singe.<br>
'''πῖλος, -ίλου (nom commun) (m)''' : pileus.<br>
'''πίμπλημι (verbe)''' : remplir.<br>
'''πίναξ, -κος (nom commun) (m)''' : tableau.<br>
'''πίπτω (verbe)''' : tomber.<br>
'''πίσσα, -εως (nom commun) (f)''' : poix.<br>
'''πιστάκη, -ης (nom commun) (f)''' : pistachier.<br>
'''πιστάκιον, -ίου (nom commun) (n)''' : pistache.<br>
'''πίστις, -εως (nom commun) (f)''' : Foi. Confiance en autrui. Ce qui fait foi. Résultat de la confiance. Moyen d’inspirer confiance, de persuader ; preuve.<br>
'''πιστότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''πιστός''.<br>
'''πιστότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''πιστός''.<br>
'''πιστός, -ή, -όν (adjectif)''' : fidèle ; loyal.<br>
'''πιστότατα, -, - (adverbe)''' : Superlatif de ''πιστῶς''.<br>
'''πιστότερον, -, - (adverbe)''' : Comparatif de ''πιστῶς''.<br>
'''πιστῶς (adverbe)''' : fidèlement ; loyalement.<br>
'''πίστωσις, -ώσεως (nom commun) (f)''' : crédit.<br>
'''πισύγγιον, ίου (nom commun) (n)''' : Diminutif de ''πίσυγγος''.<br>
'''πίσυγγος, -ύγγου (nom commun) (m)''' : cordonnier.<br>
'''πίττα, -ας (nom commun) (f)''' : Forme attique de ''πίσσα''.<br>
'''πίτα, -ας (nom commun) (f)''' : pita.<br>
'''πλαγκτός, -ή, -όν (adjectif)''' : errant.<br>
'''πλαδαρός, -ά, -όν (adjectif)''' : Forme dorienne de ''βλαδαρός''.<br>
'''πλάζω (verbe)''' : errer.<br>
'''πλᾶθος, -άθους (nom commun) (n)''' : Forme dorienne de ''πλῆθος''.<br>
'''πλακοῦς, -ύντος (nom commun) (m)''' : Gâteau ; galette.<br>
'''πλανάτας, -ου (nom commun) (m)''' : Forme dorienne de ''πλανήτης''.<br>
'''πλάνης, -τος (nom commun) (m)''' : vagabond.<br>
'''πλανήτης, -ου (nom commun) (m)''' : planète.<br>
'''πλάν (préposition)''' : Forme dorienne de ''πλήν''.<br>
'''πλανῶ (verbe)''' : vagabonder.<br>
'''πλάξ, -κός (nom commun) (f)''' : plaque.<br>
'''πλάσμα, -τος (nom commun) (n)''' : Ouvrage façonné, modelé ; figure. Modulation de la voix. Contrefaçon, imitation. Travail de composition.<br>
'''πλάσσω (verbe)''' : former, mouler.<br>
'''πλατέως (adverbe)''' : largement.<br>
'''πλατύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''πλατύς''.<br>
'''πλατύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''πλατύς''.<br>
'''πλατύς, -εῖα, -ύ (adjectif)''' : large.<br>
'''πλάττω (verbe)''' : Forme attique de ''πλάσσω''.<br>
'''πλέθρον, -ου (nom commun) (n)''' : plèthre. (env. 30 m)<br>
'''πλεῖστος, -ίστη, -ῖστον (adjectif)''' : Superlatif de ''πολύς''.<br>
'''πλείων, -ων, -ῖον (adjectif)''' : Comparatif de ''πολύς''.<br>
'''πλεῖθος, -ίθους (nom commun) (n)''' : Forme béotienne de ''πλῆθος''.<br>
'''πλῆθος, -ήθους (nom commun) (n)''' : .<br>
'''πληθύς, -ος (nom commun) (n)''' : Forme ionienne de ''πλῆθος''.<br>
'''πληθώρα, -ας (nom commun) (f)''' : plénitude.<br>
'''πλήθω (verbe)''' : remplir.<br>
'''πληκτικός, -ή, -όν (adjectif)''' : ennuyeux.<br>
'''πληκτικότης, -ητος (nom commun) (f)''' : ennui.<br>
'''πλῆκτρον, -ήκτρου (nom commun) (n)''' : plectre.<br>
'''πλῆξις, -ήξεως (nom commun) (f)''' : ennui.<br>
'''πλήν (préposition)''' : plus que.<br>
'''πληρέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''πλήρης''.<br>
'''πληρέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''πλήρης''.<br>
'''πλήρης, -ης, -ῆρες (adjectif)''' : complet ; plein.<br>
'''πλήρως (adverbe)''' : complètement ; pleinement.<br>
'''πληρῶ (verbe)''' : compléter ; emplir.<br>
'''πλήσσω (verbe)''' : Frapper, battre. (Au passif) Être battu, subir une défaite.<br>
'''πλήττω (verbe)''' : Forme attique de ''πλήσσω''.<br>
'''πλοῖον, -ίου (nom commun) (n)''' : navire, bateau ; vaisseau.<br>
'''πλούσιος, -α, -ον (adjectif)''' : riche, opulent.<br>
'''πλουσιότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''πλούσιος''.<br>
'''πλουσιότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''πλούσιος''.<br>
'''πλουσίως (adverbe)''' : richement.<br>
'''πλουτηρός, -ά, -όν (adjectif)''' : dispensateur de richesse.<br>
'''πλουτίζω (verbe)''' : (voix active) enrichir ; (voix passive) s’enrichir, être enrichi.<br>
'''πλουτίνδην (adverbe)''' : en choisissant parmi les plus riches.<br>
'''πλουτοτήριος, -α, -ον (adjectif)''' : Capable d’enrichir.<br>
'''πλουτοκρατία, -ας (nom commun) (f)''' : ploutocratie.<br>
'''πλοῦτος, -ύτου (nom commun) (m)''' : richesse, opulence.<br>
'''πλουτῶ (verbe)''' : être riche.<br>
'''πλύνω (verbe)''' : laver des vêtements.<br>
'''πνεῦμα, -ύματος (nom commun) (n)''' : Souffle. Exhalaison ; odeur. Esprit.<br>
'''πνευματικός, -ή, -όν (adjectif)''' : spirituel.<br>
'''πνευματικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''πνευματικός''.<br>
'''πνευματικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''πνευματικός''.<br>
'''πνευματικότατα, -, - (adverbe)''' : Superlatif de ''πνευματικῶς''.<br>
'''πνευματικότερον, -, - (adverbe)''' : Comparatif de ''πνευματικῶς''.<br>
'''πνευματικῶς (adverbe)''' : spirituellement.<br>
'''πνεύμων, -ονος (nom commun) (m)''' : poumon.<br>
'''πνέω (verbe)''' : souffler.<br>
'''πνιγηρός, -ά, -όν (adjectif)''' : étouffant, où l’on étouffe (Par extension) Étroit, resserré.<br>
'''πνίγω (verbe)''' : étrangler, étouffer ; suffoquer.<br>
'''πνίξ, -γός (nom commun) (f)''' : crampe.<br>
'''ποδάριον -ίου (nom commun) (n)''' : Diminutif de ''πούς''.<br>
'''πόδιον, -ίου (nom commun) (n)''' : podium.<br>
'''πόθεν (adverbe interrogatif)''' : d’où.<br>
'''πόθος, -ου (nom commun) (m)''' : Désir ; regret.<br>
'''ποῖ (adverbe interrogatif)''' : où (avec mouvement).<br>
'''ποίησις, -ήσεως (nom commun) (f)''' : création.<br>
'''ποιμήν, -ένος (nom commun) (m)''' : berger.<br>
'''ποινή, -ῆς (nom commun) (f)''' : Expiation d’un crime. Argent par lequel on paye les parents de la victime, le prix du sang. Rançon, expiation, châtiment, vengeance. Délivrance. Compensation, récompense.<br>
'''ποῖος, -ία, -ῖον (adjectif interrogatif)''' : quel.<br>
'''ποιότης, -τος (nom commun) (f)''' : qualité.<br>
'''ποιῶ (verbe)''' : faire.<br>
'''πολεμικός, -ή, -όν (adjectif)''' : guerrier.<br>
'''πολεμικῶς (adverbe)''' : belliqueusement.<br>
'''πολεμικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''πολεμικός''.<br>
'''πολεμικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''πολεμικός''.<br>
'''πολεμιστής, -οῦ (nom commun) (m)''' : guerrier.<br>
'''πολεμίστρα, -ας (nom commun) (f)''' : guerrière.<br>
'''πόλεμος, -έμου (nom commun) (m)''' : guerre.<br>
'''πολεμῶ (verbe)''' : guerroyer.<br>
'''πολιός, -ός, -όν (adjectif)''' : gris.<br>
'''πόλις, -εως (nom commun) (f)''' : ville.<br>
'''πολιτεία, -ας (nom commun) (f)''' : citoyenneté.<br>
'''πολίτευμα, -τος (nom commun) (n)''' : gouvernement.<br>
'''πολιτεύω (verbe)''' : Être citoyen. Participer au gouvernement, faire de la politique. (Au passif) Être gouverné, en parlant de l’État.<br>
'''πολίτης, -ου (nom commun) (m)''' : citoyen.<br>
'''πολιτικός, -ή, -όν (adjectif)''' : civique.<br>
'''πολῖτις, -ίτιδος (nom commun) (f)''' : citoyenne.<br>
'''πολλάκις (adverbe)''' : souvent.<br>
'''πολλῶς (adverbe)''' : .<br>
'''πολυμάθεια, -ας (nom commun) (f)''' : érudition.<br>
'''πολυμαθής, -ής, -ές (adjectif)''' : érudit.<br>
'''πολυμήτις, -, - (adjectif)''' : .<br>
'''πολυμήχανος, -η, -ον (adjectif)''' : ingénieux.<br>
'''πολύτροπος, -ος, -ον (adjectif)''' : astucieux.<br>
'''πολύς, -λλή, -ύ (adjectif)''' : nombreux.<br>
'''πολύχρωμος, -η, -ον (adjectif)''' : multicolore.<br>
'''πομφολυγοπάφλασμα, -άσματος (nom commun) (n)''' : .<br>
'''πομφόλυξ, -γος (nom commun) (m)''' : bulle ; ornement en forme de bulle, de rond. (Chimie) Oxyde de zinc.<br>
'''πομφός, -οῦ (nom commun) (m)''' : verrue.<br>
'''πονηρία, -ας (nom commun) (f)''' : ruse.<br>
'''πονηρός, -ά, -όν (adjectif)''' : mauvais, méchant ; pervers.<br>
'''πονηρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''πονηρός''.<br>
'''πονηρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''πονηρός''.<br>
'''πονηρῶς (adjectif)''' : mauvaisement, méchamment ; perversement.<br>
'''πόποι (interjection)''' : hélas.<br>
'''πορδή, -ῆς (nom commun) (f)''' : pet. (gaz intestinal)<br>
'''πορθμεῖον, -ίου (nom commun) (n)''' .<br>
'''πορθμεύς, -έως (nom commun) (m)''' nocher.<br>
'''πορθμεύω (verbe)''' .<br>
'''πορθμός, -οῦ (nom commun) (m)''' : détroit.<br>
'''πορνεία, -ας (nom commun)''' : Fornication, prostitution.<br>
'''πορνεῖον, -ίου (nom commun) (n)''' lupanar.<br>
'''πόρνευμα, -ύµατος (nom commun (n)''' : .<br>
'''πόρνευσις, -ύσεως (nom commun) (f)''' : .<br>
'''πορνεύτρια, -ας (nom commun) (f)''' : .<br>
'''πορνεύω (verbe)''' : prostituer.<br>
'''πόρνη, -ης (nom commun) (f)''' : Prostituée, putain.<br>
'''πορνικός, -ή, -όν (adjectif)''' : de lupanar.<br>
'''πορνοϐοσκεῖον, -ίου (nom commun) (n)''' : lupanar.<br>
'''πορνοϐοσκία, -ας (nom commun) (f)''' : prostitution.<br>
'''πορνοϐοσκός, -οῦ (nom commun) (m)''' : tenancier.<br>
'''πορνοϐοσκῶ (verbe)''' : gérer un lupanar.<br>
'''πορνοδιάκονος, -όνου (nom commun) (m/f)''' : .<br>
'''πορνοδιδάσκαλος, -άλου (nom commun) (m) ''' : .<br>
'''πορνοδύτης, -ου (nom commun) (m)''' .<br>
'''πορνοφίλης, -ου (nom commun) (m)''' : amateur de prostituées.<br>
'''πορνογενής, -ής, -ές (adjectif)''' : .<br>
'''πορνογέννητος, -ος, -ον (adjectif)''' : né dans un lupanar.<br>
'''πορνογράφος, -ου (nom commun) (m)''' : pornographe.<br>
'''πορνοκοπέω (verbe)''' : .<br>
'''πορνοκοπία, -ας (nom commun) (f)''' .<br>
'''πορνοκόπος, -ου (nom commun) (m)''' : client de lupanar.<br>
'''πορνομανής, -ής, -ές''' : furieux contre la prostitution.<br>
'''πόρνος, -ου (nom commun) (m)''' : Prostitué ; gigolo.<br>
'''πόρπη, -ης (nom commun) (f)''' : broche (bijou).<br>
'''πόρ, -δός (nom commun) (m)''' : Forme laconienne de ''πούς''.<br>
'''πόρρω (adverbe)''' : Forme attique de ''πρόσω''.<br>
'''πορφύρεος, -α, -ον (adjectif)''' : (chez Homère) Sombre. (grec post-homérique) Pourpre.<br>
'''πορφύρω (verbe)''' : Bouillir, fulminer ; rougir.<br>
'''πόσις, -εως (nom commun) (f)''' : action de boire.<br>
'''πόσις, -ιος (nom commun) (m)''' : époux ; mari.<br>
'''πόσσις, -ιος (nom commun) (m)''' : Forme poétique de ''πόσις''. (au sens d’« époux », « mari »)<br>
'''πόσος, -η, -ον (adjectif interrogatif)''' : combien.<br>
'''ποσότης, -τος (nom commun) (f)''' : quantité.<br>
'''πός, -δός (nom commun) (m)''' : Forme dorienne de ''πούς''.<br>
'''ποταμός, -οῦ (nom commun) (m)''' : fleuve.<br>
'''πότε (adverbe interrogatif)''' : quand.<br>
'''ποτέ (adverbe)''' (Devient ''ποτ᾽'' devant un mot commençant par une voyelle à esprit doux, et ''ποθ᾽'' devant un mot commençant par une voyelle à esprit rude.) : jamais.<br>
'''ποτήριον, -ίου (nom commun) (n)''' : coupe. (récipient)<br>
'''ποτήρ, -ῆρος (nom commun) (m)''' : verre. (récipient)<br>
'''πότμος, -ου (nom commun) (m)''' : destin ; sort.<br>
'''πότνια, -ίης (nom commun) (f)''' : épouse ; maîtresse.<br>
'''πούς, -δός (nom commun) (m)''' : pied.<br>
'''ποῦ (adverbe interrogatif)''' : où (sans mouvement) ; comment.<br>
'''πρᾶγμα, -άγματος (nom commun) (n)''' : Chose, évènement ; affaire. Fait.<br>
'''πραγματικός, -ή, -όν (adjectif)''' : évènementiel ; factuel.<br>
'''πραγματικῶς (adverbe)''' : évènementiellement ; factuellement.<br>
'''πραγματικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''πραγματικός''.<br>
'''πραγματικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''πραγματικός''.<br>
'''πραικόκιον, -ίου (nom commun) (n)''' : abricot.<br>
'''πράκτωρ, -ορος (nom commun) (m)''' : agent.<br>
'''πρᾶξις, -άξεως (nom commun) (f)''' : action.<br>
'''πραπίς, -δος (nom commun) (f)''' : intelligence.<br>
'''πράσινος, -ίνη, -άσινον (adjectif)''' : vert poireau.<br>
'''πράσσω (verbe)''' : faire, pratiquer.<br>
'''πρατήρ, -ῆρος (nom commun) (m)''' : vendeur.<br>
'''πρᾶτος, -άτη, -ᾶτον (adjectif)''' : Forme dorienne de ''πρῶτος''.<br>
'''πράττω (verbe)''' : Forme attique de ''πράσσω''.<br>
'''πρέπω (verbe)''' : .<br>
'''πρέσϐυς, -έως (nom commun) (m)''' : Envoyé ; député ; ambassadeur.<br>
'''πρεσϐυτέριον, -ίου (nom commun) (n)''' : assemblée des anciens.<br>
'''πρῆγμα, -ήγματος (nom commun) (f)''' : Forme ionienne de ''πρᾶγμα''.<br>
'''πρηκτήρ, -ός (nom commun) (m)''' : Forme homérique et ionienne de ''πράκτωρ''.<br>
'''πρῆξις, -ήξεως (nom commun) (f)''' : Forme homérique et ionienne de ''πρᾶξις''.<br>
'''πρήσσω (verbe)''' : Forme homérique et ionienne de ''πράσσω''.<br>
'''πρῆχμα, -ήχματος (nom commun) (f)''' : Autre forme ionienne de ''πρᾶγμα''.<br>
'''πρῖγκιψ, -ίγκιπος (nom commun) (m)''' : prince. (chef d’une principauté)<br>
'''πριγκιπίσσα, -ας (nom commun) (f)''' : princesse. (cheffe d’une principauté)<br>
'''πρίν (conjonction)''' : avant que.<br>
'''πριόνιον, -ίου (nom commun) (n)''' : diminutif de ''πρίων''.<br>
'''πρίων, -ονος (nom commun) (m)''' : scie.<br>
'''προάστειος, -, - (adjectif)''' : suburbain.<br>
'''προάστειον, -ίου (nom commun) (n)''' : banlieue.<br>
'''προϐαίνω (verbe)''' : marcher en avant.<br>
'''προϐατίνα, -ας (nom commun) (f)''' : brebis.<br>
'''πρόϐατον, -άτου (nom commun) (n)''' : mouton.<br>
'''πρόϐλημα, -ήματος (nom commun) (n)''' : problème.<br>
'''πρόγνωσις, -ώσεως (nom commun) (f)''' : .<br>
'''πρόγονος, -όνου (nom commun) (m)''' : ancêtre.<br>
'''προπάτωρ, -ορος (nom commun) (m)''' : ancêtre.<br>
'''πρόγραμμα, -άμματος (nom commun) (n)''' : notice écrite publique.<br>
'''προγράφω (verbe)''' : écrire une notice publique.<br>
'''προδίδωμι (verbe)''' : trahir.<br>
'''προδότης, -ου (nom commun) (m)''' : traître.<br>
'''προδοτικός, -ή, -όν (adjectif)''' : traître.<br>
'''προδοσία, -ας (nom commun) (f)''' : trahison.<br>
'''προοίμιον, -ίου (nom commun) (n)''' : prélude.<br>
'''προκινῶ (verbe)''' : .<br>
'''πρόκοιτος, -ίτου (nom commun) (m)''' : chambellan.<br>
'''προλαμϐάνω (verbe)''' : prévoir.<br>
'''πρόληψις, -ήψεως (nom commun) (f)''' : prévention.<br>
'''πρόλογος, -όγου (nom commun) (m)''' : préface.<br>
'''προσϐάλλω (verbe)''' : .<br>
'''προσϐολή, -ῆς (nom commun) (f)''' : .<br>
'''προσευχή, -ῆς (nom commun) (f)''' : prière.<br>
'''προσεύχομαι (verbe)''' : Adresser une prière. (Absolu) Adorer, prier, supplier.<br>
'''προσήλωσις, -ώσεως (nom commun) (f)''' : attachemeent.<br>
'''πρόσθεν (adverbe)''' Devant ; en avant.<br>
'''προσιτός, -ή, -όν (adjectif)''' : abordable.<br>
'''πρόσειμι (verbe)''' : aborder.<br>
'''προσθιγγάνω (verbe)''' : .<br>
'''προσκέφαλον, -άλου (nom commun) (m)''' : oreiller.<br>
'''πρόσκοπος, -όπου (nom commun) (m)''' : éclaireur.<br>
'''πρόσκοπευω (verbe)''' : .<br>
'''προσποιῶ (verbe)''' : .<br>
'''προσωποποιία, -ας (nom commun) (f)''' : personnification.<br>
'''πρόσω (adverbe)''' : .<br>
'''πρός (adverbe ; préposition)''' : Auprès, à côté ; en outre.<br>
'''πρότατος, -άτα, -άτετον (adjectif)''' : Superlatif de ''πρῶτος''.<br>
'''πρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''πρῶτος''.<br>
'''προτί (adverbe ; préposition)''' : Forme homérique de ''πρός''.<br>
'''ποτί (adverbe ; préposition)''' : Forme homérique et dorienne de ''πρός''.<br>
'''ποί (adverbe ; préposition)''' : Forme dorienne de ''πρός''.<br>
'''πορτί (adverbe ; préposition)''' : Forme crétoise de ''πρός''.<br>
'''πός (adverbe ; préposition)''' : Forme arcado-chypriote de ''πρός''.<br>
'''πρές (adverbe ; préposition)''' : Forme éolienne de ''πρός''.<br>
'''πέρτι (adverbe ; préposition)''' : Forme pamphylienne de ''πρός''.<br>
'''πρᾶος, -ος, -ᾷον (adjectif)''' : doux ; calmant.<br>
'''πραότης, -τος (nom commun) (f)''' : Douceur, bonté ; facilité de caractère.<br>
'''πραύς, -εῖα, -ύ (adjectif)''' : Variante de ''πρᾶος''.<br>
'''πραΰς, -εΐα, -ΰ (adjectif)''' : Forme poétique de ''πρᾶος''.<br>
'''πρήξιμον, -ίματος (nom commun) (n)''' : Enflure, gonflement ; ballonnement, boursouflure, fluxion.<br>
'''πρῖγκιψ, -ίγκιπος (nom commun) (m)''' : prince (chef d’une principauté).<br>
'''πριγκίπισσα, -ας (nom commun) (f)''' : princesse (cheffe d’une principauté).<br>
'''πρό (adverbe ; préposition)''' devant.<br>
'''προ- (préfixe)''' pré-.<br>
'''παράρτημα, -ήματος (nom commun) (n)''' : annexe.<br>
'''προζύμιον, -ίου (nom commun) (n)''' : .<br>
'''προζυμίτης, -ου (nom commun) (m)''' : .<br>
'''πρόσθεσις, -έσεως (nom commun) (f)''' : addition.<br>
'''προσφορά, -ᾶς (nom commun) (f)''' : offre.<br>
'''προσωπεῖον, -ίου (nom commun) (n)''' : masque.<br>
'''προσωπίς, -δος (nom commun) (f)''' : masque.<br>
'''προσ- (préfixe)''' : Vers, à ; Auprès. En outre, encore. Excessivement ; tout à fait.<br>
'''προσαρτῶ (verbe)''' : annexer.<br>
'''προστίθημι (verbe)''' : additionner ; ajouter.<br>
'''προσομοίωσις, -ώσεως (nom commun) (f)''' : simulation.<br>
'''προσποιῶ (verbe)''' : simuler.<br>
'''πρότανις, -άνεως (nom commun) (m)''' : Forme éolienne de ''πρύτανις''.<br>
'''προῦμνον, -ύμνου (nom commun) (n)''' : prune.<br>
'''προφατεία, -ας (nom commun) (f)''' : Forme dorienne de ''προφητεία''.<br>
'''προφάτης, -ου (nom commun) (m)''' : Forme dorienne de ''προφήτης''.<br>
'''προφητεία, -ας (nom commun) (f)''' : prophétie.<br>
'''προφήτης, -ου (nom commun) (m)''' : prophète.<br>
'''προφυλακτικός, -ή, -όν (adjectif)''' : .<br>
'''προφυλάξις, -εως (nom commun) (f)''' : .<br>
'''προφυλάσσω (verbe)''' : .<br>
'''πρύμνα, -ας (nom commun) (f)''' : poupe.<br>
'''πρύμνη, -ης (nom commun) (f)''' : Forme ionienne de ''πρύμνα''.<br>
'''πρυτανεῖον, -ίου (nom commun) (n)''' : (À Athènes) mairie.<br>
'''πρύτανις, -άνεως (nom commun) (m)''' : prytane.<br>
'''πρωΐ (adverbe)''' : tôt.<br>
'''πρῴ (adverbe)''' : Forme attique de ''πρωΐ''.<br>
'''πρωκτός, -οῦ (nom commun) (m)''' : anus.<br>
'''πρωκτόσοφος, -όφου (nom commun) (m/f)''' : Expert en débauche contre nature.<br>
'''πρῷρα, -ῴρας (nom commun) (f)''' : proue.<br>
'''πρωταγωνιστής, -οῦ (nom commun) (m)''' : protagoniste.<br>
'''πρῶτος, -ώτη, -ῶτον (adjectif)''' : premier.<br>
'''πρωτόκολλον, -όλλου (nom commun) (n)''' : frontispice.<br>
'''πτελέα, -ας (nom commun) (f)''' : orme.<br>
'''πτέρινος, -ίνη, -ινον (adjectif)''' : .<br>
'''πτέρις, -δος (nom commun) (f)''' : fougère.<br>
'''πτερόεις, -εσσα, -εν (adjectif)''' : ailé.<br>
'''πτερόν, -οῦ (nom commun) (n)''' : aile.<br>
'''πτέρνα, -ης (nom commun) (f)''' : talon. (partie postérieure du pied)<br>
'''πτέρυξ, -υγος (nom commun) (f)''' : Aile ; nageoire.<br>
'''πτηνόν, -οῦ (nom commun) (n)''' : oiseau.<br>
'''πτηνοπέδιλος, -ίλου (nom commun) (m)''' : talaria.<br>
'''πτισάνη, -ης (nom commun) (f)''' : tisane.<br>
'''πτίσσω (verbe)''' : .<br>
'''πτίττω (verbe)''' : Forme attique de ''πτίσσω''.<br>
'''πτόλεμος, -έμου (nom commun) (m)''' : Forme homérique de ''πόλεμος''.<br>
'''πτολίεθρον, -έθρου (nom commun) (n)''' : citadelle.<br>
'''πτόλις, -εως (nom commun) (f)''' : Forme homérique de ''πόλις''.<br>
'''πτύον, -ου (nom commun) (n)''' : pelle.<br>
'''πτύω (verbe)''' : cracher.<br>
'''πτῶσις, -ώσεως (nom commun) (f)''' : chute.<br>
'''πτώσσω (verbe)''' : chuter.<br>
'''πτωχεία, -ας (nom commun) (f)''' : pauvre.<br>
'''πτωχεῖον, -ίου (nom commun) (n)''' : hospice.<br>
'''πτώχευσις, -ύσεως (nom commun) (f)''' : banqueroute.<br>
'''πτωχεύω (verbe)''' : faire banqueroute.<br>
'''πτωχός, -ή, -όν (adjectif)''' : pauvre.<br>
'''πτωχοτροφεῖον, -ίου (nom commun) (n)''' : hospice.<br>
'''πτωχοτρόφος, -ου (nom commun) (m)''' : ptôchotrophe.<br>
'''πτωχότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''πτωχός''.<br>
'''πτωχότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''πτωχός''.<br>
'''πτωχῶς (adverbe)''' : pauvrement.<br>
'''πυγή, -ῆς (nom commun) (f)''' : fesse.<br>
'''πυγηφαvοῦς, -οῦ (nom commun) (m)''' : mooning. (Action de montrer ses fesses nues en baissant son pantalon et en se penchant en avant.)<br>
'''πυγίδιον, -ίου (nom commun) (n)''' : croupion.<br>
'''πυγίζω (verbe)''' : sodomiser.<br>
'''πυγμαῖος, -ία -ῖον (adjectif)''' : pygmée.<br>
'''πυγμή, -ῆς (nom commun) (f)''' : poing.<br>
'''πύελος, -έλου (nom commun) (f)''' : bassin (os).<br>
'''πυθμήν, -ένος (nom commun) (m)''' : fond.<br>
'''πυκνός, -ή, -όν (adjectif)''' : dense, sagace.<br>
'''πυκνότατα, -, - (adverbe)''' : Superlatif de ''πυκνῶς''.<br>
'''πυκνότερον, -, - (adverbe)''' : Comparatif de ''πυκνῶς''.<br>
'''πυκνότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''πυκνός''.<br>
'''πυκνότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''πυκνός''.<br>
'''πυκνότης, -τος (nom commun) (f)''' : densité ; sagacité.<br>
'''πυκνῶς (adverbe)''' : densément ; sagacement.<br>
'''πύλη, -ης (nom commun) (f)''' : (Au singulier) battant de porte. (Au pluriel) porte.<br>
'''πυλωρός, -οῦ (nom commun) (m)''' : Portier. (Anatomie) pylore.<br>
'''πύνδαξ, -κος (nom commun) (m)''' : fond.<br>
'''πυνθάνομαι (verbe)''' : chercher à savoir ; s’enquérir ; s’informer de ; apprendre en s’informant ; apprendre<br>
'''πυξίς, -δος (nom commun) (f)''' : pyxide.<br>
'''πύον, -ου (nom commun) (n)''' : pus.<br>
'''πυραμίς, -δος (nom commun) (f)''' : pyramide.<br>
'''πυργίτης, -ου (nom commun) (m)''' : moineau.<br>
'''πύργος, -ου (nom commun) (m)''' : Tour. (Par extension) Enceinte garnie de tours. (Par extension) Citadelle, remparts. (Par analogie) Sorte de bataillon carré.<br>
'''πύρινος, -η, -ον (adjectif)''' : enflammé.<br>
'''πυρινότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''πύρινος''.<br>
'''πυρινότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''πύρινος''.<br>
'''πύρινως (adverbe)''' : .<br>
'''πῦρ, -ός (nom commun) (n)''' : feu.<br>
'''πυρρός, -ά, -όν (adjectif)''' : .<br>
'''πυρρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''πυρρός''.<br>
'''πυρρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''πυρρός''.<br>
'''πυρρῶς (adverbe)''' : .<br>
'''πύσμα, -τος (nom commun) (n)''' : question ouverte.<br>
'''πύστις, -εως (nom commun) (f)''' : Enquête, question. Apprentissage, chose apprise.<br>
'''πυστός, -ή, -όν (adjectif)''' : .<br>
'''πωγώνιον, -ίου (nom commun) (m)''' : menton.<br>
'''πώγων, -ονος (nom commun) (m)''' : Barbe ; barbe d’animal, de plante. Barbelure. Langue de feu.<br>
'''πωλέω (verbe)''' : vendre.<br>
'''πώλης, -ου (nom commun) (m)''' : vendeur.<br>
'''πωλητής, -οῦ (nom commun) (m)''' : vendeur.<br>
'''πῶμα, -ώματος (nom commun) (n)''' : bouchon.<br>
'''πῶυ, -ώεος (nom commun) (n)''' : Troupeau d’ovins ou de chèvres.<br>
'''πῶς (adverbe)''' : comment.<br>
'''Παιάν, -ᾶνος (nom commun) (m)''' : péan. (nom propre) : Péan.<br>
'''Πακτωλός, -οῦ (nom propre) (m)''' : Pactole.<br>
'''Παλαιὰ Διαθήκη (locution nominale) (f)''' : Ancien Testament.<br>
'''Παλλάδιον, -ίου (nom propre) (n)''' : Palladium.<br>
'''Πάλλας, -αντος (nom propre) (m)''' : Pallas (nom masculin).<br>
'''Παλλάς, -δος (nom propre) (f)''' : Pallas (nom féminin).<br>
'''Πάλμυρα, -ύρας (nom propre) (f)''' : Palmyre.<br>
'''Πάμφιλος, -ίλου (nom propre) (m)''' : Pamphile.<br>
'''Πανάκεια, -ας (nom propre) (f)''' : [[wikt:Panacée|Panacée]].<br>
'''Πανδώρα, -ας (nom propre) (f)''' : [[wikt:Pandore|Pandore]].<br>
'''Πάν, -ός (nom propre) (m)''' : [[wikt:Pan|Pan]].<br>
'''Πανοῦργος, -ύργου (nom propre) (m)''' : Panurge.<br>
'''Πάνθειον, -ίου (nom propre) (m)''' : Panthéon.<br>
'''Παρθένος, -ου (nom propre) (f)''' : Vierge.<br>
'''Παρθενών, -ῶνος (nom commun) (m)''' : Parthénon.<br>
'''Παρθία, -ας (nom propre) (f)''' : Parthie.<br>
'''Πάρθος, -ου (nom commun) (m)''' : Parthe.<br>
'''Πάρις, -δος (nom propre) (m)''' : Pâris.<br>
'''Παρμενίδης, -ου (nom propre) (m)''' : Parménide.<br>
'''Πασιφάη, -ης (nom propre) (f)''' : Pasiphaé.<br>
'''Παῦλος, -ύλου (nom propre) (m)''' : Paul.<br>
'''Παῦνι (nom propre) (m)''' : Payni.<br>
'''Παυσανίας, -ου (nom propre) (m)''' : Pausanias.<br>
'''Παχώμιος, -ίου (nom propre) (m)''' : Pacôme.<br>
'''Παχών (nom propre) (m)''' : Pachon.<br>
'''Πελοπόννησος, -ήσου (nom propre) (f)''' : Péloponnèse.<br>
'''Πέλοψ, -πος (nom propre) (m)''' : Pélops.<br>
'''Πεμφρηδώ, -οῦς (nom propre) (f)''' : Pemphrédo (une des Grées).<br>
'''Πεντάτευχος, -ύχου (nom propre) (f)''' : Pentateuque.<br>
'''Πέργαμον, -άμου (nom propre) (n)''' : Pergame.<br>
'''Περίϐοια, -ίας (nom propre) (f)''' : Périboée.<br>
'''Περικλῆς, -έους (nom propre) (m)''' : Périclès.<br>
'''Περίλεως, -εω (nom propre) (m)''' : Périléos.<br>
'''Περσέπολις, -όλεως (nom propre) (f)''' : Persépolis.<br>
'''Περσεπολίτης, -ου (nom commun) (m)''' : Persépolitain.<br>
'''Πέρσης, -ου (nom commun) (m)''' : Perse.<br>
'''Περσίς, -δος (nom propre) (f)''' : Perse.<br>
'''Περσίς, -δος (nom commun) (f)''' : Perse.<br>
'''Πετεφρής, -οῦ (nom propre) (m)''' : Potiphar.<br>
'''Πήγασος, -άσου (nom propre) (m)''' : Pégase.<br>
'''Πηλεύς, -έως (nom propre) (m)''' : Pélée.<br>
'''Πηνειός, -οῦ (nom propre) (m)''' : Pénée.<br>
'''Πηνελόπη, -ης (nom propre) (f)''' : Pénélope.<br>
'''Πίστις, -εως (nom propre) (f)''' : Pistis.<br>
'''Πιτθεύς, -έως (nom propre) (m)''' : Pitthée. (Grand-père de Thésée.)<br>
'''Πλάτων, -ωνος (nom propre) (m)''' : Platon.<br>
'''Πλευρών, -ῶνος (nom propre) (m/f)''' : Pleuron.<br>
'''Πλούταρχος, -άρχου (nom propre) (m)''' : Plutarque.<br>
'''Πλουτεύς, -έως (nom propre) (m)''' : Autre nom de Pluton.<br>
'''Πλουτίς, -δος (nom propre) (f)''' : Ploutis.<br>
'''Πλοῦτος, -ύτου (nom propre) (m)''' : [[wikt:Ploutos|Ploutos]].<br>
'''Πλούτων, -ωνος (nom propre) (m)''' : [[wikt:Pluton|Pluton]].<br>
'''Πόθος, - (nom propre) (m)''' : [[wikt:Pothos|Pothos]].<br>
'''Ποικίλη, -ης (nom propre) (f)''' : Pœcile. (portique au nord de l’Agora d’Athènes.)<br>
'''Πολύϐιος, -ίου (nom propre) (m)''' : Polybe.<br>
'''Πολυμνία, -ας (nom propre) (f)''' : Polymnie.<br>
'''Πολύαινος, -ίνου (nom propre) (m)''' : Polyen.<br>
'''Πολυνείκης, -ους (nom propre) (m)''' : Polynice.<br>
'''Πολυπήμων, -ονος (nom propre) (m)''' : Polypémon (véritable nom de Procuste).<br>
'''Πολύφημος, -ήμου (nom propre) (m)''' : Polyphème.<br>
'''Πομπηία, -ας (nom propre) (f)''' : Pompéi.<br>
'''Πομπήϊος, -ΐου (nom propre) (m)''' : Pompée.<br>
'''Ποσειδώνιος, - (nom propre) (m)''' : Posidonios (Philosophe grec stoïcien né à Apamée en 135 av. J.-C. et mort à Rome en 51 av. J.-C. .).<br>
'''Ποσειδάν, - (nom propre) (m)''' : Forme dorienne de ''Ποσειδῶν''.<br>
'''Ποσειδάων, - (nom propre) (m)''' : Forme homérique de ''Ποσειδῶν''.<br>
'''Ποσειδέων, - (nom propre) (m)''' : Forme ionienne de ''Ποσειδῶν''.<br>
'''Ποσειδῶν, -ῶνος (nom propre) (m)''' : [[wikt:Poséidon|Poséidon]].<br>
'''Ποτειδάν, - (nom propre) (m)''' : Autre forme dorienne de ''Ποσειδῶν''.<br>
'''Ποτείδαν, - (nom propre) (m)''' : Forme éolienne de ''Ποσειδῶν''.<br>
'''Ποτειδᾶς, - (nom propre) (m)''' : Forme de ''Ποσειδῶν''.<br>
'''Ποτειδάων, - (nom propre) (m)''' : Forme crétoise et béotienne de ''Ποσειδῶν''.<br>
'''Πραξιτέλης, -ους (nom propre) (m)''' : Praxitèle.<br>
'''Πρίαμος, -άμου (nom propre) (m)''' : Priam.<br>
'''Πρόκνη, -ης (nom propre) (f)''' : Procné.<br>
'''Προκρούστης, -ου (nom propre) (m)''' : Procuste (surnom de Polypémon.).<br>
'''Προκύων, -ός (nom propre) (m)''' : Procyon.<br>
'''Προμηθεύς, -έως (nom propre) (m)''' : Prométhée.<br>
'''Προξένος, -ρου (nom propre) (m)''' : Proxenos.<br>
'''Προυσίας, -ου (nom propre) (m)''' : Prusias.<br>
'''Πρωταγόρας, -ου (nom propre) (m)''' : Protagoras.<br>
'''Πυγμαλίων, -ωνος (nom propre) (m)''' : Pygamlion.<br>
'''Πυθαγόρας, -ου (nom propre) (m)''' : Pythagore.<br>
'''Πυθόπολις, -όλεως (nom propre) (f)''' : Antioche du Méandre.<br>
'''Πύθων, -ωνος (nom propre) (m)''' : Python.<br>
'''Πυθώ, -οῦς (nom propre) (f)''' : Ancien nom de Delphes.<br>
'''Πυλάδας, -ου (nom propre) (m)''' : Forme dorienne de ''Πυλάδης''.<br>
'''Πυλάδης, -ου (nom propre) (m)''' : Pylade.<br>
'''Πύραμος, -άμου (nom propre) (m)''' : Pyrame.<br>
'''Πυρετός, -οῦ (nom propre) (m)''' : (fleuve d’Europe de l’Est).<br>
'''Πυργοπολυνείκης, -ους (nom propre) (m)''' : Pyrgopolinice.<br>
'''Πύργος, -ου (nom propre) (m)''' : Pyrgos.<br>
'''Πύρρα, -ας (nom propre) (f)''' : Pyrrha.<br>
'''Πύρρος, -ου (nom propre) (m)''' : Pyrrhus.<br>
'''Πύρρων, -ωνος (nom propre) (m)''' : Pyrrhon.<br>
'''Πῶλος, -ώλου (nom propre) (m)''' : Polos.<br>
==Ρ==
'''ῥᾶ (nom commun) (n)''' : rhubarbe.<br>
'''ῥαϐϐί (nom commun) (m)''' : rabbin.<br>
'''ῥαϐδομαντεία, -ας (nom commun) (f)''' : Divination pratiquée grâce à une baguette.<br>
'''ῥαϐδονόμος, -ου (nom commun) (m)''' : licteur.<br>
'''ῥαϐδοῦχος, -ύχου (nom commun) (m)''' : licteur.<br>
'''ῥάϐδος, -ου (nom commun) (f)''' : baguette.<br>
'''ῥαγάς, -δος (nom commun) (f)''' : .<br>
'''ῥαιϐηδόν, -οῦ (nom commun) (n)''' .<br>
'''ῥαιϐοειδής, -ής, -ές (adjectif) ''' .<br>
'''ῥαιϐόκρανος, -άνου (nom commun) (m)''' .<br>
'''ῥαιϐοσκελής, -ής, -ές (adjectif) ''' .<br>
'''ῥαιϐός, -ή, -όν (adjectif)''' : tendu.<br>
'''ῥαιϐότης, -τος (nom commun) (f)''' .<br>
'''ῥαιϐῶς (adverbe)''' : de façon tendue.<br>
'''ῥαιϐῶ (verbe)''' tendre.<br>
'''ῥαδινός, -ή -όν (adjectif)''' : souple.<br>
'''ῥᾴδιος, -α, -ον (adjectif)''' : facile.<br>
'''ῥᾳδίως (adverbe)''' : facilement.<br>
'''ῥᾷστος, -, - (adjectif)''' : Superlatif de ''ῥᾴδιος''.<br>
'''ῥᾴων, -, - (adjectif)''' : Comparatif de ''ῥᾴδιος''.<br>
'''ῥάμνος, -ου (nom commun) (f)''' nerprun épineux.<br>
'''ῥάμφος, -ους (nom commun) (n)''' : bec (d’un oiseau).<br>
'''ῥάξ, -γός (nom commun) (m)''' : téton.<br>
'''ῥάπισμα, -ίσματος (nom commun) (n)''' : gifle.<br>
'''ῥαπίς, -δος (nom commun) (f)''' : verge ; bâton.<br>
'''ῥάπτης, -ου (nom commun) (n)''' : tailleur (de vêtements).<br>
'''ῥαπτός, -ή, -όν (adjectif)''' : cousu.<br>
'''ῥάπτω (verbe)''' : coudre.<br>
'''ῥάπυς, -ος (f)''' : navet.<br>
'''ῥάσσω (verbe)''' : .<br>
'''ῥάττω (verbe)''' : Forme attique de ''ῥάσσω''.<br>
'''ῥαφανιδῶ (verbe)''' : .<br>
'''ῥαφανίδωσις, -ώσεως (nom commun) (f)''' : .<br>
'''ῥαφανός, -οῦ (nom commun) (m)''' : radis.<br>
'''ῥαφεύς, -έως (nom commun) (m)''' : couturier.<br>
'''ῥαφή, -ῆς (nom commun) (f)''' : couture.<br>
'''ῥαφίς, -δος (nom commun) (f)''' : Aiguille, poinçon. Aiguille (poisson de mer).<br>
'''ῥάφυς, -ος (nom commun) (f)''' : Variante de ''ῥάπυς''.<br>
'''ῥαχίζω (verbe)''' : .<br>
'''ῥάχις, -εως (nom commun) (f)''' : épine dorsale.<br>
'''ῥαχιστής, -οῦ (nom commun) (m)''' : .<br>
'''ῥαχιστός, -ός, -όν (adjectif)''' : .<br>
'''ῥαχίτης, -ης, -ες, (adjectif)''' : .<br>
'''ῥαχός, -ή, -όν, (adjectif)''' : superficiel.<br>
'''ῥαχός, -οῦ (nom commun) (m)''' : .<br>
'''ῥάψις, -εως (nom commun) (f)''' : .<br>
'''ῥαψῳδία, -ας (nom commun) (f)''' : récitation d’un poème épique.<br>
'''ῥαψῳδός, -οῦ (nom commun) (m)''' : barde.<br>
'''ῥέδδω (verbe)''' : Forme béotienne et dorienne de ''ῥέζω''.<br>
'''ῥέζω (verbe)''' : Travailler, faire, exécuter.<br>
'''ῥέπω (verbe)''' : incliner.<br>
'''ῥεῦμα, -ύματος (nom commun) (n)''' : rhume.<br>
'''ῥευματίζομαι (verbe)''' : souffrir de rhumatismes.<br>
'''ῥευματισμός, -οῦ (nom commun) (m)''' : rhumatisme.<br>
'''ῥευστός, -ή -όν (adjectif)''' : liquide.<br>
'''ῥέω (verbe)''' : couler.<br>
'''ῥῆγμα, -ήγματος (nom commun) (n)''' : .<br>
'''ῥήγνυμι (verbe)''' : .<br>
'''ῥῆμα, -ήματος (nom commun) (n)''' : verbe.<br>
'''ῥῆξις, -ήξεως (nom commun) (f)''' : déchirure.<br>
'''ῥῆον, -ήου (nom commun) (n)''' : rhubarbe.<br>
'''ῥῆσις, -ήσεως (nom commun) (f)''' : citation.<br>
'''ῥήσσω (verbe)''' : Forme ionienne de ''ῥάσσω''.<br>
'''ῥητορικός, -ή, -όν (adjectif)''' : oratoire.<br>
'''ῥητορικῶς (adverbe)''' : oratoirement.<br>
'''ῥητορικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ῥητορικός''.<br>
'''ῥητορικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ῥητορικός''.<br>
'''ῥητορικώτατα, -, - (adverbe)''' : Superlatif de ''-ικῶς''.<br>
'''ῥητορικώτερον, -, - (adverbe)''' : Comparatif de ''-ικῶς''.<br>
'''ῥητός, -ή, -όν (adjectif)''' : Dit ; dicible, (Mathématiques) rationnel.<br>
'''ῥήτρα, -ας (nom commun) (f)''' : clause.<br>
'''ῥήτωρ, -ορος (nom commun) (m/f)''' : orateur.<br>
'''ῥηχός, -ή -όν (adjectif)''' : Forme ionienne de ''ῥαχός''.<br>
'''ῥῖγος, -ίγους (nom commun) (n)''' : frisson.<br>
'''ῥιγῶ (verbe)''' : frissonner.<br>
'''ῥίζα, -ης (nom commun) (f)''' : racine.<br>
'''ῥινόκερως, -έρωτος (nom commun) (m)''' : rhinocéros.<br>
'''ῥίον, -ου (nom commun) (n)''' : sommet.<br>
'''ῥιπή, -ῆς (nom commun) (f)''' : .<br>
'''ῥίπτω (verbe)''' : lancer, jeter ; projet.<br>
'''ῥίς, -νός (nom commun) (f)''' : nez.<br>
'''ῥίψασπις, -δος (nom commun) (f)''' : .<br>
'''ῥίψις, -πός (nom commun) (f)''' : entrelac.<br>
'''ῥιπίζω (verbe)''' : éventer.<br>
'''ῥιπίς, -δος (nom commun) (f)''' : éventail.<br>
'''ῥοά, -ᾶς (nom commun) (f)''' : Forme dorienne de ''ῥοή''.<br>
'''ῥοή, -ῆς (nom commun) (f)''' : écoulement.<br>
'''ῥοδόεις, -εσσα, -εν (adjectif)''' : rosé.<br>
'''ῥοδοέντως (adverbe)''' : .<br>
'''ῥοδοέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''ῥοδόεις''.<br>
'''ῥοδοέστερος, -έρα, -ερον (adjectif)''' : Comparatif de ''ῥοδόεις''.<br>
'''ῥόδον, -ου (nom commun) (n)''' : rose (fleur).<br>
'''ϝρόδον, -ου (nom commun) (n)''' : Forme éolienne de ''ῥόδον''.<br>
'''ῥοιά, -ᾶς (nom commun) (f)''' : grenade (fruit).<br>
'''ῥόκα, -ας (nom commun) (f)''' : roquette (plante).<br>
'''ῥόμος, -ου (nom commun) (m)''' : ver.<br>
'''ῥομφαία, -ας (nom commun) (f)''' : estramaçon.<br>
'''ῥόπαλον, -άλου (nom commun) (n)''' : gourdin.<br>
'''ῥοφῶ (verbe)''' : sucer.<br>
'''ῥύαξ, -κος (nom commun) (n)''' : ruisseau.<br>
'''ῥύγχος, -ους (nom commun) (n)''' : Trompe, museau. Bec d’un oiseau.<br>
'''ῥυθμός, -οῦ (nom commun) (m)''' : mouvement régulier.<br>
'''ῥύμη, -ης (nom commun) (f)''' : rue.<br>
'''ῥυπαίνω (verbe)''' : polluer.<br>
'''ῥύπανσις, -άνσεως (nom commun) (f)''' : pollution.<br>
'''ῥυπαρός, -ά, -όν (adjectif)''' : crasseux.<br>
'''ῥυπαρῶς (adverbe)''' : crasseusement.<br>
'''ῥυπαρώτατα, -, - (adverbe)''' : Superlatif de ''ῥυπαρῶς''.<br>
'''ῥυπαρώτερον, -, - (adverbe)''' : Comparatif de ''ῥυπαρῶς''.<br>
'''ῥυπαρώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ῥυπαρός''.<br>
'''ῥυπαρώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ῥυπαρός''.<br>
'''ῥύπος, -ου (nom commun) (m)''' : polluant.<br>
'''ῥυσμός, -οῦ (nom commun) (m)''' : Forme ionienne de ''ῥυθμός''.<br>
'''ῥυτίς, -δος (nom commun) (f)''' : ride.<br>
'''ῥωγμή, -ῆς (nom commun) (f)''' : fissure.<br>
'''ῥώθων, -ος (nom commun) (m)''' : narine.<br>
'''ῥωμαϊκός, -ή, -όν (adjectif)''' : romain.<br>
'''ῥωμαϊκῶς (adverbe)''' : romainement.<br>
'''ῥωμαϊκώτατα, -, - (adverbe)''' : Superlatif de ''ῥωμαϊκῶς''.<br>
'''ῥωμαϊκώτερον, -, - (adverbe)''' : Comparatif de ''ῥωμαϊκῶς''.<br>
'''ῥωμαϊκώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ῥωμαϊκός''.<br>
'''ῥωμαϊκώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ῥωμαϊκός''.<br>
'''ῥωμαλέος, -α, -ον (adjectif)''' : fort, robuste (en parlant des personnes) ; solide (en parlant des choses).<br>
'''ῥωμαλεότης, -τος (nom commun) (f)''' : force, robustesse (en parlant des personnes) ; solidité (en parlant des choses).<br>
'''ῥώμη, -ης (nom propre) (f)''' : force.<br>
'''ῥώομαι (verbe)''' : se précipiter, fondre sur.<br>
'''ῥωσικός, -ή, -όν (adjectif)''' : russe.<br>
'''ῥώξ, -γός (nom commun) (m)''' : Forme ionienne de ''ῥάξ''.<br>
'''ῥῶ (nom commun) (n)''' : rhô.<br>
'''Ῥαάβ (nom propre) (f)''' : Rahab.<br>
'''Ῥα (nom propre) (m)''' : Rê.<br>
'''Ῥᾶ (nom propre) (n)''' : Volga.<br>
'''Ῥαδάμανθυς, -άνθυος (nom propre) (m)''' : Rhadamanthe.<br>
'''Ῥαθούρης, -ου (nom propre) (m)''' : Niouserrê.<br>
'''Ῥαθωτις, -ίδος (nom propre) (m)''' : Toutânkhamon.<br>
'''Ῥαμέσσης, -ου (nom propre) (m)''' : Ramsès.<br>
'''Ῥαμνοῦς, -ντος (nom propre) (m)''' : Rhamnonte.<br>
'''Ῥαφαήλ (nom propre) (m)''' : Raphaël.<br>
'''Ῥεϐέκκα, -ας (nom propre) (f)''' : Rebecca.<br>
'''Ῥῆνος, -ήνου (nom propre) (m)''' : Rhin.<br>
'''Ῥίον, -ου (nom propre) (m)''' : Río (ville de Grèce).<br>
'''Ῥοδανός, -οῦ (nom propre) (m)''' : Rhône.<br>
'''Ῥόδη, -ης (nom propre) (f)''' : Rhodé.<br>
'''Ῥόδιος, - (nom propre) (m)''' : Rhodien.<br>
'''Ῥοδόπη, -ης (nom propre) (f)''' : Rhodope.<br>
'''Ῥόδος, -ου (nom propre) (f)''' : Rhodes.<br>
'''Ῥούθ (nom propre) (f)''' : Ruth.<br>
'''Ῥοῦφος, -ύφου (nom propre) (m)''' : Rufus.<br>
'''Ῥωμαῖος, -ίου (nom commun) (m)''' : Romain.<br>
'''Ῥώμη, -ης (nom propre) (f)''' : Rome.<br>
'''Ῥῶμος, -ώμου (nom propre) (m)''' : Rémus.<br>
'''Ῥώμυλος, -ύλου (nom propre) (m)''' : Romulus.<br>
'''Ῥωξάνη, -ης (nom propre) (f)''' : Roxane (Épouse d'Alexandre le Grand.)<br>
'''Ῥωσία, -ας (nom propre) (f)''' : Russie.<br>
'''Ῥωσίς, -δος (nom propre) (f)''' : Russe.<br>
'''Ῥῶς, -ώσος (nom commun) (m)''' : Russe.<br>
==Σ==
'''σάϐϐατον, -άτου (nom commun) (n)''' : shabbat.<br>
'''σαϐϐατισμός, -οῦ (nom commun) (n)''' : sabbatisme.<br>
'''σαδδουκαῖος, -ίου (nom commun) (m)''' : sadducéen.<br>
'''σάγμα, -τος (nom commun) (n)''' : bât.<br>
'''σάκκος, -ου (nom commun) (m)''' : sac.<br>
'''σάκος, -ους (nom commun) (n)''' : bouclier.<br>
'''σάκχαρις, -άρεως (nom commun) (f)''' : sucre.<br>
'''σάλπιγξ, -γoς (nom commun) (f)''' : trompette.<br>
'''σᾶμα, -άματος (nom commun) (n)''' : Forme dorienne de ''σῆμα''.<br>
'''σάμερον (adverbe)''' : Forme dorienne de ''σήμερον''.<br>
'''σάμιος, -ος, -ον (adjectif)''' : samosien.<br>
'''σαμψήρα, -ας (nom commun) (f)''' : cimeterre.<br>
'''σάνδαλον, -άλου (nom commun) (n)''' : sandale.<br>
'''σανίς, -δος (nom commun) (f)''' : planche.<br>
'''σαρδονικός, -ή, -όν (adjectif)''' : sarde.<br>
'''σαρδονικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''σαρδονικός''.<br>
'''σαρδονικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''σαρδονικός''.<br>
'''σαρδονικότατα, -, - (adverbe)''' : Superlatif de ''σαρδονικῶς''.<br>
'''σαρδονικότερον, -, - (adverbe)''' : Comparatif de ''σαρδονικῶς''.<br>
'''σαρδονικῶς (adverbe)''' : sardoniquement.<br>
'''σαρκικός, -ή, -όν (adjectif)''' : charnel.<br>
'''σαρκικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''σαρκικός''.<br>
'''σαρκικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''σαρκικός''.<br>
'''σαρκικότατα, -, - (adverbe)''' : Superlatif de ''σαρκικῶς''.<br>
'''σαρκικότερον, -, - (adverbe)''' : Comparatif de ''σαρκικῶς''.<br>
'''σαρκικῶς (adverbe)''' : charnellement.<br>
'''σάρκινος, -ίνη, -άρκινον (adjectif)''' : charnu.<br>
'''σάρξ, -κός (nom commun) (f)''' : chair.<br>
'''σατυρίασις, -άσεως (nom commun) (f)''' : satyriasis.<br>
'''σατυρικός, -ή, -όν (adjectif)''' : satyrique.<br>
'''σατύριον, -ίου (nom commun) (n)''' : satyrion.<br>
'''σατυρίσκος, -ου (nom commun) (m)''' : satyreau.<br>
'''σάτυρος, -ύρου (nom commun) (m)''' : satyre.<br>
'''σατυρώδης, -ης, -ες (adjectif)''' : satyrode.<br>
'''σαύρα, -ας (nom commun) (f)''' : lézard ; quéquette.<br>
'''σαύρη, -ης (nom commun) (f)''' : Forme ionienne de ''σαύρα''.<br>
'''σαυροματικός, -ή, -όν (adjectif)''' : sarmate.<br>
'''σαυροματικῶς (adverbe)''' : .<br>
'''σαυροματικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σαυροματικός''.<br>
'''σαυροματικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σαυροματικός''.<br>
'''σαῦρος, -ύρου (nom commun) (m)''' : lézard.<br>
'''σάπφειρος, -ίρου (nom commun) (f)''' : saphir.<br>
'''σαφέστερος, -άτη, -έστατον (adjectif)''' : Superlatif de ''σαφής''.<br>
'''σαφέστατος, -έρα, -έσερον (adjectif)''' : Comparatif de ''σαφής''.<br>
'''σαφής, -ής, -ές (adjectif)''' : Clair, évident ; manifeste.<br>
'''σαφῶς (adverbe)''' : Clairement, évidemment ; manifestement.<br>
'''σεϐάζω (verbe)''' : respecter.<br>
'''σεϐασμός, -οῦ (nom commun) (m)''' : respect.<br>
'''σεϐαστός, -ός, -όν (adjectif)''' : respectable ; respecté.<br>
'''σέϐομαι (verbe)''' : Craindre les dieux. Vénérer.<br>
'''σείριος, -ίου (nom commun) (m)''' : destructeur.<br>
'''σελάνα, -ας (nom commun) (f)''' : Forme dorienne de ''σελήνη''.<br>
'''σελάννα, -ας (nom commun) (f)''' : Forme éolienne de ''σελήνη''.<br>
'''σέλας, -τος (nom commun) (n)''' : Éclat, lumière ; lueur brillante.<br>
'''σελάχιον, -ίου (nom commun) (n)''' : raie (poisson).<br>
'''σέλαχος, -άχους (nom commun) (n)''' : requin.<br>
'''σελήνη, -ης (nom commun) (f)''' : lune.<br>
'''σεληνιακός, -ή, -όν (adjectif)''' : lunaire.<br>
'''σέλινον, -ίνου (nom commun) (n)''' : céleri.<br>
'''σελίς, -δος (nom commun) (f)''' : page. (Face d'une feuille de papier, de parchemin, de vélin, servant à l'écriture.)<br>
'''σεμίδαλις, -άλεως (nom commun) (f)''' : semoule.<br>
'''σεμνός, -ή, -όν (adjectif)''' : modeste.<br>
'''σεμνότατα, -, - (adverbe)''' : Superlatif de ''σεμνῶς''.<br>
'''σεμνότερον, -, - (adverbe)''' : Comparatif de ''σεμνῶς''.<br>
'''σεμνότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''σεμνός''.<br>
'''σεμνότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''σεμνός''.<br>
'''σεμνότης, -τος (nom commun) (f)''' : modestie.<br>
'''σεμνῶς (adverbe)''' : modestement.<br>
'''σῆμα, -ήματος (nom commun) (n)''' : signe.<br>
'''σημαντικός -ή -όν (adjectif)''' : significatif.<br>
'''σημαντικῶς (adverbe)''' : significativement.<br>
'''σημαντικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σημαντικός''.<br>
'''σημαντικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σημαντικός''.<br>
'''σημαντικώτατα, -, - (adverbe)''' : Superlatif de ''σημαντικῶς''.<br>
'''σημαντικώτερον, -, - (adverbe)''' : Comparatif de ''σημαντικῶς''.<br>
'''σημεῖον, -ίου (nom commun) (n)''' : signal.<br>
'''σήμερον (adverbe)''' : aujourd’hui.<br>
'''σημύδα, -ης (nom commun) (f)''' : bouleau.<br>
'''σῆραγξ, -ήραγγος (nom commun) (f)''' : tunnel.<br>
'''σήσαμον, άμου (nom commun) (n)''' : sésame.<br>
'''σής, -τός (nom commun) (m)''' : mite.<br>
'''σητόδοκις, -εως (nom commun) (f)''' : papillon.<br>
'''σῆψις, -ήψεως (nom commun) (f)''' : putréfaction.<br>
'''σιαγών, -όνος (nom commun) (f)''' : mâchoire.<br>
'''σιηγών, -όνος (nom commun) (f)''' : Forme ionienne de ''σιαγών''.<br>
'''σῖγμα (nom commun) (n)''' : sigma.<br>
'''σικάριος, -ίου (nom commun) (m)''' : assassin.<br>
'''σικελικός, -ή, -όν (adjectif)''' : sicilien.<br>
'''σικυός, -οῦ (nom commun) (m)''' : concombre.<br>
'''σικυώνιος, -ος, -ον (adjectif)''' : .<br>
'''σίλφιον, -ίου (nom commun) (n)''' : silphium.<br>
'''σιμός, -ή, -όν (adjectif)''' : Camus ; montant.<br>
'''σιμῶ (verbe)''' : lever le nez.<br>
'''σίναπι, -άπεως (nom commun) (n)''' : moutarde (plante).<br>
'''σιός, -οῦ (nom commun) (m)''' : Forme laconienne de ''θεός''.<br>
'''σίσυς, -ος (nom commun) (m)''' : fourrure.<br>
'''-σις, -εως (suffixe) (f)''' : Suffixe formant des noms d'action.<br>
'''σῖτος, -ίτου (nom commun) (m)''' : Grain, à la fois le blé et l'orge. (Par extension) Toute nourriture végétale (par opposition à la viande ou la boisson). Pension alimentaire. (Droit athénien) Distribution de blé aux indigents.<br>
'''σιωπή, -ῆς (nom commun) (f)''' : calme, silence.<br>
'''σιωπηρός, -ός, -όν (adjectif)''' : silencieux, tacite.<br>
'''σιωπηρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''σαρκικός''.<br>
'''σιωπηρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''σαρκικός''.<br>
'''σιωπηρότατα, -, - (adverbe)''' : Superlatif de ''σαρκικῶς''.<br>
'''σιωπηρότερον, -, - (adverbe)''' : Comparatif de ''σαρκικῶς''.<br>
'''σιωπηρῶς (adverbe)''' : tacitement.<br>
'''σιωπηρότης, -τος (nom commun) (f)''' : tacité.<br>
'''σιωπῶ (verbe)''' : se taire.<br>
'''σκαιός, -ά, -όν (adjectif)''' : qui est à gauche.<br>
'''σκάνδαλον, -άλου (nom commun) (n)''' : Piège placé sur le chemin.<br>
'''σκανδάληθρον, -ήθρου (nom commun) (n)''' : .<br>
'''σκανδαλίζω (verbe)''' : Placer un piège sur le chemin.<br>
'''σκαπανεύς, -έως (nom commun) (m)''' : .<br>
'''σκαπάνη, -ης (nom commun) (f)''' : .<br>
'''σκᾶπτρον, -άπτρου (nom commun) (n)''' : Forme dorienne de ''σκῆπτρον''.<br>
'''σκάπτω (verbe)''' : creuser.<br>
'''σκεδάννυμι (verbe)''' : disperser, répandre.<br>
'''σκέλος, -ους (nom commun) (n)''' : (Anatomie) Jambe de l’homme et des animaux. (Au pluriel) Les murs (entre Athènes et le Pirée, entre Mégare et Nisæa).<br>
'''σκεπτικός, -ή, -όν (adjectif)''' : observateur.<br>
'''σκεπτικῶς (adverbe)''' : sceptiquement.<br>
'''σκεπτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σκεπτικός''.<br>
'''σκεπτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σκεπτικός''.<br>
'''σκέπτομαι (verbe)''' : Considérer ; examiner avec soin.<br>
'''σκευαρίδιον, -ίου (nom commun) (n)''' : .<br>
'''σκευάριον, -ίου (nom commun) (n)''' : .<br>
'''σκευωρία, -ας (nom commun) (f)''' : machination.<br>
'''σκεῦος, -ύους (nom commun) (n)''' : ustensile.<br>
'''σκέψις, -εως (nom commun) (f)''' : pensée. (opération de l’intelligence.)<br>
'''σκηπτός, -οῦ (nom commun) (m)''' : ouragan.<br>
'''σκῆπτρον, -ήπτρου (nom commun) (n)''' : Étai, sceptre.<br>
'''σκήπτω (verbe)''' : Étayer, soutenir. Brandir.<br>
'''σκίουρος, -ύρου (nom commun) (m)''' : écureuil.<br>
'''σκολιός, -ά, -όν (adjectif)''' : tordu.<br>
'''σκολόπαξ, -άκος (nom commun) (m)''' : bécasse.<br>
'''σκολόπενδρα, -ένδρας (nom commun) (f)''' : scolopendre.<br>
'''σκολοπένδριον, -ίου (nom commun) (n)''' : Diminutif de ''σκολόπενδρα''.<br>
'''σκόλοψ, -πος (nom commun) (m)''' : écharde.<br>
'''σκόπελος, -έλου (nom commun) (m)''' : écueil.<br>
'''σκοπῶ (verbe)''' : observer.<br>
'''σκόροδον, -όδου (nom commun) (n)''' : ail.<br>
'''σκορπιός, -οῦ (nom commun) (m)''' : scorpion.<br>
'''σκοτεινός, -ή, -όν (adjectif)''' : obscur.<br>
'''σκοτεινῶς (adverbe)''' : obscurément.<br>
'''σκοτεινώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σκοτεινός''.<br>
'''σκοτεινώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σκοτεινός''.<br>
'''σκότιος, -ία, -ότιον (adjectif)''' : bâtard.<br>
'''σκότος, -ου (nom commun) (m)''' : obscurité.<br>
'''σκυθικός, -ή, -όν (adjectif)''' : scythe.<br>
'''σκυθικῶς (adverbe)''' : .<br>
'''σκυθικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σκυθικός''.<br>
'''σκυθικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σκυθικός''.<br>
'''σκύλαξ, -κος (nom commun) (m)''' : chiot.<br>
'''σκυλεύω (verbe)''' : dépouiller.<br>
'''σκύλλω (verbe)''' : Déchirer, troubler.<br>
'''σκῦλον, -ύλου (nom commun) (n)''' : butin.<br>
'''σκῦτος, -ύτου (nom commun) (m)''' : .<br>
'''σκώληξ, -κος (nom commun) (m)''' : ver.<br>
'''σκῶμμα, -ώμματος (nom commun) (n)''' : .<br>
'''σκωραμίς, -δος (nom commun) (f)''' : pot de chambre.<br>
'''σκωρία, -ας (nom commun) (f)''' : scorie.<br>
'''σκῶρ, -ατός (nom commun) (n)''' : excrément.<br>
'''σκώψ, -πός (nom commun) (m)''' : hibou.<br>
'''σμάραγδος, -άγδου (nom commun) (f)''' : émeraude.<br>
'''σμάω (verbe)''' : frotter, nettoyer.<br>
'''σμῆγμα, -ήγματος (nom commun) (n)''' : savon, détergent ; onguent.<br>
'''σμήχω (verbe)''' : essuyer.<br>
'''σμῖλαξ, -ίλακος (nom commun) (f)''' : if.<br>
'''σμίλη, -ης (nom commun) (f)''' : Ciseau. Bistouri, lancette.<br>
'''σμύραινα, -ίνης (nom commun) (f)''' : murène.<br>
'''σμῶδιξ, -ώδιγγος (nom commun) (f)''' : contusion.<br>
'''σμώχω (verbe)''' : frotter.<br>
'''σοϐαρός, -ή, -όν (adjectif)''' : Effrayant. Fuyant, rapide. Hautain, dédaigneux ; pompeux.<br>
'''σοϐαρῶς (adverbe)''' : Effrayamment, rapidement. Hautainement, dédaigneusement ; pompeusement.<br>
'''σοϐαρώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σοϐαρός''.<br>
'''σοϐαρώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σοϐαρός''.<br>
'''σοϐῶ (verbe)''' : Chasser, effrayer les oiseaux. Bouger rapidement.<br>
'''σαπρός, -ή, -όν (adjectif)''' : Pourri, putride<br>
'''σαπρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''σαπρός''.<br>
'''σαπρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''σαπρός''.<br>
'''σαπρῶς (adverbe)''' : putridement.<br>
'''σός, -ή, -όν (adjectif possessif)''' : ton.<br>
'''σοφία, -ας (nom commun) (f)''' : sagesse.<br>
'''σοφίη, -ης (nom commun) (f)''' : Forme ionienne de ''σοφία''.<br>
'''σοφός, -ή, -όν (adjectif)''' : Habile. (En parlant de l’intelligence ou du caractère) Prudent, sage. (En particulier) Initié à la sagesse. Ingénieux, fin, rusé.<br>
'''σοφῶς (adverbe)''' : Habilement, sagement. Ingénieusement, finement.<br>
'''σοφώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σοφός''.<br>
'''σοφώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σοφός''.<br>
'''σπαδωνισμός, -οῦ (nom commun) (m)''' : castration.<br>
'''σπάδων, -ος (nom commun) (m)''' : castrat ; eunuque.<br>
'''σπάθη, -ης (nom commun) (f)''' : épée.<br>
'''σπαθίον, -ου (nom commun) (m)''' : Diminutif de ''σπάθη''.<br>
'''σπανακόν, -οῦ (nom commun) (n)''' : épinard.<br>
'''σπάναξ, -κος (nom commun) (m)''' : épinard.<br>
'''σπάνιος, -ία, -ιον (adjectif)''' : rare.<br>
'''σπανίως (adverbe)''' : rarement.<br>
'''σπανιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σπάνιος''.<br>
'''σπανιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σπάνιος''.<br>
'''σπαργάνιον, -ίου (nom commun) (n)''' : rubanier.<br>
'''σπάργανον, -άνου (nom commun) (n)''' : lange.<br>
'''σπάργω (verbe)''' : langer.<br>
'''σπαρτιατικός, -ή, -όν (adjectif)''' : spartiate.<br>
'''σπασμός, -οῦ (nom commun) (m)''' : spasme.<br>
'''σπαστικός, -ή, -όν (adjectif)''' : .<br>
'''σπάω (verbe)''' : briser.<br>
'''σπεῖρα, -ίρας (nom commun) (f)''' : spire.<br>
'''σπεῖρον, -ίρου (nom commun) (n)''' : .<br>
'''σπέος, -ους (nom commun) (n)''' : grotte.<br>
'''σπεύδω (verbe)''' : se hâter.<br>
'''σπήλαιον, -ίου (nom commun) (n)''' : caverne, grotte ; cavité.<br>
'''σπῆλυγξ, -ήλυγγος (nom commun) (m)''' : caverne, antre ; grotte.<br>
'''σπογγιά, -ᾶς (nom commun) (f)''' : éponge.<br>
'''σπόγγος, -ου (nom commun) (m)''' : éponge ; (anatomie) amygdale.<br>
'''σποδός, -οῦ (nom commun) (m)''' : cendre.<br>
'''σπολεύς, -έως (nom commun) (m)''' : sorte de pain.<br>
'''σπονδεῖος, -ίου (nom commun) (m)''' : spondée.<br>
'''σπόνδυλος, -ύλου (nom commun) (m)''' : Forme attique de ''σφόνδυλος''.<br>
'''σπουδαῖος, -ία, -ῖον (adjectif)''' : important.<br>
'''σπουδή, -ῆς (nom commun) (f)''' : hâte.<br>
'''στάδιον, -ου (nom commun) (n)''' : stade. (env. 180 m)<br>
'''σταγών, -όνος (nom commun) (f)''' : goutte.<br>
'''στάζω (verbe)''' : couler goutte à goutte.<br>
'''σταθερός, -ή, -όν (adjectif)''' : fixe, constant, ferme.<br>
'''στακτός, -οῦ (nom commun) (m)''' : cendre.<br>
'''στάλα, -ας (nom commun) (f)''' : Forme dorienne de ''στήλη''.<br>
'''στάλαγμα, -άγματος (nom commun) (n)''' : goutte.<br>
'''σταλαγμός, -οῦ (nom commun) (m)''' : écoulement, égouttage.<br>
'''σταλάσσω (verbe)''' : tomber, couler.<br>
'''στάλλα, -ας (nom commun) (f)''' : Forme éolienne de ''στήλη''.<br>
'''στάσις, -εως (nom commun) (f)''' : .<br>
'''σταυρός, -οῦ (nom commun) (m)''' : croix.<br>
'''σταυρόω (verbe)''' : crucifier.<br>
'''σταύρωσις, -ώσεως (nom commun) (f)''' : crucifixion.<br>
'''σταφυλή, -ῆς (nom commun) (f)''' : grappe de raisin mûr.<br>
'''σταφυλίς, -δος (nom commun) (f)''' : luette.<br>
'''στέαρ, -ατος (nom commun) (n)''' : Graisse compacte ; lard ; suif. Graisse.<br>
'''στεατοπυγός, -ός, -όν (adjectif)''' : Qui a de grosses fesses.<br>
'''στέγασις, -άσεως (nom commun) (f)''' : accommodation.<br>
'''στεγανός, -ή, -όν (adjectif)''' : étanche, imperméable.<br>
'''στεγανῶς (adverbe)''' : .<br>
'''στεγανώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''στεγανός''.<br>
'''στεγανώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''στεγανός''.<br>
'''στέγος, -ους (nom commun) (n)''' : Abri. Toit. Maison. Tombeau. Urne funéraire.<br>
'''στέγω (verbe)''' : Couvrir. Supporter, résister.<br>
'''στεῖρος, -ίρα, -ῖρον (adjectif)''' : stérile.<br>
'''στειρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''στεῖρος''.<br>
'''στειρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''στεῖρος''.<br>
'''στείρως (adverbe)''' : stérilement.<br>
'''στελεά, -ᾶς (nom commun) (f)''' : axe, pôle.<br>
'''στελεόν, -οῦ (nom commun) (n)''' : manche.<br>
'''στέλεχος, -έχους (nom commun) (n)''' : .<br>
'''στέλλω (verbe)''' : .<br>
'''στέμμα, -τος (nom commun) (n)''' : guirlande.<br>
'''στεμματηφορῶ (verbe)''' : .<br>
'''στεμματιαῖον, -ίου (nom commun) (n)''' : .<br>
'''στεμματία, -ας (nom commun) (f)''' : .<br>
'''στεμματοφορία, -ας (nom commun) (n)''' : .<br>
'''στεμματοφόρος, -ό, -όν (adjectif)''' : étroit, resserré.<br>
'''στεμματῶ (verbe)''' : .<br>
'''στενός, -ή, -όν (adjectif)''' : étroit, resserré.<br>
'''στενῶς (adverbe)''' : étroitement.<br>
'''στενώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''στενός''.<br>
'''στενώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''στενός''.<br>
'''στερεός, -ά, -όν (adjectif)''' : ferme, dur.<br>
'''στερέωμα, -ώματος (nom commun) (n)''' : firmament.<br>
'''στερεῶς (adverbe)''' : fermement, durement.<br>
'''στερεώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''στερεός''.<br>
'''στερεώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''στερεός''.<br>
'''στερεῶ (verbe)''' : .<br>
'''στέφανος, -άνου (nom commun) (m)''' : cercle d'une armée sur un champ de bataille. Couronne.<br>
'''στέφω (verbe)''' : couronner.<br>
'''στηθόδεσμος -ου (nom commun) (m)''' : .<br>
'''στῆθος, -ήθους (nom commun)''' : poitrine.<br>
'''στήλη, -ης (nom commun) (f)''' : stèle.<br>
'''στήνιον, -ίου (nom commun) (n)''' : sein.<br>
'''στιϐάς, -δος (nom commun) (f)''' : .<br>
'''στίϐι, -τος (nom commun) (n)''' : antimoine.<br>
'''στιγμή, -ῆς (nom commun) (f)''' : moment.<br>
'''στοργή, -ῆς (nom commun) (f)''' : amour familial.<br>
'''στοιχεῖον, -ίου (nom commun) (n)''' : élément.<br>
'''στοιχειωδέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''στοιχειώδης''.<br>
'''στοιχειωδέστερος, -έρη, -έστερον (adjectif)''' : Comparatif de ''στοιχειώδης''.<br>
'''στοιχειώδης, -ης, -ῶδες (m)''' : élémentaire.<br>
'''στοιχειωδῶς (adverbe)''' : élémentairement.<br>
'''στολίζω (verbe)''' : .<br>
'''στόμα, -τος (nom commun) (n)''' : bouche.<br>
'''στομάχιον, -ίου (nom commun) (n)''' : estomac.<br>
'''στώμυλμα, -τος (nom commun) (n)''' : bavard.<br>
'''στοχάζομαι (verbe)''' : Conjecturer. Viser.<br>
'''στοχάς, -δος (nom commun) (f)''' : cible.<br>
'''στοχαστής, -οῦ (nom commun) (m)''' : Conjectureur, penseur.<br>
'''στοχαστικός, -ή, -όν (adjectif)''' : Qui vise bien, qui tend directement vers. Habile à conjecturer, conjectural.<br>
'''στοχαστικῶς (adverbe)''' : conjecturalement.<br>
'''στοχαστικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''στοχαστικός''.<br>
'''στοχαστικώτερος, -έρη, -ώτερον (adjectif)''' : Comparatif de ''στοχαστικός''.<br>
'''στόχος, -ου (nom commun) (m)''' : Cible, but, point visé. Conjecture.<br>
'''στορέννυμι (verbe)''' : étendre, recouvrir.<br>
'''στραγγαλίζω (verbe)''' : étrangler.<br>
'''στραγγαλίς, -δος (nom commun) (f)''' : nœud.<br>
'''στραγγαλοῦμαι (verbe)''' : tordre.<br>
'''στραγγίζω (verbe)''' : essorer.<br>
'''στραγγουρία, -ας (nom commun) (f)''' : strangurie.<br>
'''στράγξ, -γός (nom commun) (f)''' : goutte.<br>
'''στρατός, -οῦ (nom commun) (m)''' : armée.<br>
'''στρατηγός, -οῦ (nom commun) (m)''' : général.<br>
'''στραταγός, -οῦ (nom commun) (m)''' : Forme arcadienne et dorienne de ''στρατηγός''.<br>
'''στρατήγημα, -ήματος (nom commun) (n)''' : stratagème.<br>
'''στρατηγία, -ας (nom commun) (f)''' : stratégie.<br>
'''στρέμμα, -τος (nom commun) (n)''' : Tournure ; rouleau. Conspiration.<br>
'''στρεπτικός, -ή, -όν (adjectif)''' : .<br>
'''στρεπτικῶς (adverbe)''' : .<br>
'''στρεπτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''στρεπτικός''.<br>
'''στρεπτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''στρεπτικός''.<br>
'''στρεπτικώτατα, -, - (adverbe)''' : Superlatif de ''στρεπτικῶς''.<br>
'''στρεπτικώτερον, -, - (adverbe)''' : Comparatif de ''στρεπτικῶς''.<br>
'''στρεπτός, -ή, -όν (adjectif)''' : Tourné ; docile.<br>
'''στρεπτῶς (adverbe)''' : docilement.<br>
'''στρεπτώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''στρεπτός''.<br>
'''στρεπτώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''στρεπτός''.<br>
'''στρέφω (verbe)''' : Tourner, retourner.<br>
'''στρέψις, -εως (nom commun) (f)''' : .<br>
'''στρίζω (verbe)''' : strider.<br>
'''στρίξ, -γός (nom commun) (f)''' : chouette.<br>
'''στρόταγος, -άγου (nom commun) (m)''' : Forme éolienne de ''στρατηγός''.<br>
'''στρατιώτης, -ου (nom commun) (m)''' : soldat.<br>
'''στρόϐιλος, -ίλου (nom commun) (m)''' : Ce qui tourne ou tournoie. Toupie. Tourbillon, ouragan. Objet divers en spirale ou de forme conique. Pomme de pin ou fruit des arbres résineux. Coquillage en spirale. Enroulement du hérisson sur lui-même. Qui tournoie en spirale.<br>
'''στροϐιλόω (verbe)''' : tourner.<br>
'''στραγγός, -ή, -όν (adjectif)''' tordu.<br>
'''στρογγύλλω (verbe)''' : arrondir.<br>
'''στρογγύλος, -η, -ον (adjectif)''' : arrondi.<br>
'''στρογγυλότης, -τος (nom commun) (f)''' : rondeur.<br>
'''στρουθιοκάμηλος, -ήλου (nom commun) (m)''' : autruche.<br>
'''στρουθίον, -ου (nom commun) (n)''' : moineau.<br>
'''στροφή, -ῆς (nom commun) (f)''' : tour.<br>
'''στρόφιγξ, -γος (nom commun) (m)''' : charnière.<br>
'''στρόφιον, -ίου (nom commun) (n)''' : strophium.<br>
'''στρόφος, -ου (nom commun) (m)''' : corde.<br>
'''στύφω (verbe)''' : contracter.<br>
'''στῦψις, -ύψεως (nom commun) (f)''' : contraction.<br>
'''στρῶμα, -ώματος (nom commun) (n)''' : matelas.<br>
'''στρώννυμι (verbe)''' : .<br>
'''στυγερός, -ά, -όν (adjectif)''' : horrible.<br>
'''στυγερώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''στυγερός''.<br>
'''στυγερώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''στυγερός''.<br>
'''στυγερῶς (adverbe)''' : horriblement.<br>
'''στῦλος, -ύλου (nom commun) (m)''' : Pilier, colonne.<br>
'''στῦσις, -ύσεως (nom commun) (f)''' : érection (action physiologique).<br>
'''στύω (verbe)''' : avoir une érection.<br>
'''συγάτηρ, -τρός (nom commun) (f)''' : Forme dorienne de ''θυγάτηρ''.<br>
'''συγγνώμη, -ης (nom commun) (f)''' : .<br>
'''συγγιγνώσκω (verbe)''' : .<br>
'''συγκέντρωσις, -ώσεως (nom commun) (f)''' : concentration.<br>
'''συγκεντρῶ (verbe)''' : concentrer.<br>
'''συγκεράννυμι (verbe)''' : .<br>
'''συγκρητισμός, -οῦ (nom commun) (m)''' : .<br>
'''συγκινῶ (verbe)''' : émouvoir.<br>
'''συγκίνησις, -ήσεως (nom commun) (f)''' : émotion.<br>
'''συγκινητικός, -ή, -όν (adjectif)''' : émouvant.<br>
'''συγκρίνω (verbe)''' : comparer.<br>
'''σύγκρισις, -ίσεως (nom commun) (f)''' : comparaison.<br>
'''συγκριτικός, -ή, -όν (adjectif)''' : comparatif.<br>
'''σύγκρουσις, -ύσεως (nom commun) (f)''' : collision.<br>
'''συγχώρησις, -εως (nom commun) (f)''' : .<br>
'''συγχωρητέος, -α, -ον (adjectif)''' : .<br>
'''συζήτησις, -ήσεως (nom commun) (f)''' : discussion, conversation ; débat.<br>
'''συζητῶ (verbe)''' : discuter, converser ; débattre.<br>
'''σῦκον, -ύκου (nom commun) (n)''' : Figue ; vulve.<br>
'''συκοφάντης, -ου (nom commun) (m)''' : Délateur, calomniateur. Chicaneur de mauvaise foi.<br>
'''συκοφαντικός, -ή, -όν (adjectif)''' : flagorneur.<br>
'''συκοφαντικῶς (adverbe)''' : flagorneusement.<br>
'''συκοφαντικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''συκοφαντικός''.<br>
'''συκοφαντικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''συκοφαντικός''.<br>
'''συκοφαντικώτατα, -, - (adverbe)''' : Superlatif de ''συκοφαντικῶς''.<br>
'''συκοφαντικώτερον, -, - (adverbe)''' : Comparatif de ''συκοφαντικῶς''.<br>
'''σύκχος, -ους (nom commun) (n)''' : pantoufle.<br>
'''συλάω (verbe)''' : Saisir, prendre ; emporter. Dépouiller, prendre les armes de son ennemi mort. Piller.<br>
'''σύλη, -ης (nom commun) (f)''' : Prise ; saisie.<br>
'''σύλησις, -ήσεως (nom commun) (f)''' : profanation.<br>
'''σύλον, -ου (nom commun) (n)''' : Forme attique de ''ξύλον''.<br>
'''συλλαμϐάνω (verbe)''' : .<br>
'''σύλληψις, -ήψεως (nom commun) (f)''' : Action de prendre ensemble. Compréhension. Réunion par prononciation de deux voyelles. Action de s’emparer, de saisir. Conception dans le sein de la mère. Assistance, secours.<br>
'''συμϐάλλω (verbe)''' : .<br>
'''σύμϐασις, -άσεως (nom commun) (f)''' : convention.<br>
'''συμϐίωσις, -ώσεως (nom commun) (f)''' : vie commune.<br>
'''συμϐιῶ (verbe)''' : vivre ensemble.<br>
'''συμϐόλαιον, -ίου (nom commun) (n)''' : contrat.<br>
'''συμϐολικός, -ή, -όν (adjectif)''' : relatif aux signes de reconnaissance.<br>
'''σύμϐολον, -όλου (nom commun) (n)''' : signe de reconnaissance.<br>
'''συμμετρία, -ας (nom commun) (f)''' : bonne proportion.<br>
'''συμπάθεια, -ίας (nom commun) (f)''' : Communauté de sentiments ou d’impressions. (Philosophie), (Terme stoïcien) Rapport de certaines choses entre elles.<br>
'''συμπεραίνω (verbe)''' : accomplir, finir, trancher, décider.<br>
'''συμπέρασμα, -άσματος (nom commun) (n)''' : conclusion.<br>
'''συμπερασματικός, -ή, -όν (adjectif)''' : conclusif.<br>
'''συμπερασματικῶς (adverbe)''' : conclusivement.<br>
'''συμπεριφέρομαι (verbe)''' : se comporter.<br>
'''συμπεριφορά, -ᾶς (nom commun) (f)''' : comportement.<br>
'''συμπεριφορικός, -ή, -όν (adjectif)''' : comportemental.<br>
'''συμπεριφορισμός, -οῦ (nom commun) (m)''' : comportementalisme.<br>
'''συμπεριφοριστής, -οῦ (nom commun) (m)''' : comportementaliste.<br>
'''συμπίνω (verbe)''' : festoyer.<br>
'''συμπονῶ (verbe)''' : compatir.<br>
'''συμπόσιον, -ίου (nom commun) (n)''' : Banquet, festin. (Collectif) Les convives. Salle de festin.<br>
'''σύμπτωμα, -ώματος (nom commun) (n)''' : Accident, malchance ; symptôme.<br>
'''σύμπτωσις, -ώσεως (nom commun) (f)''' : coïncidence.<br>
'''συμπίπτω (verbe)''' : coïncider.<br>
'''συμφορά, -ᾶς (nom commun) (f)''' : désastre.<br>
'''συμφοράζω (verbe)''' : .<br>
'''συμφοραίνω (verbe)''' : .<br>
'''συμφορηδόν, -οῦ (nom commun) (n)''' : .<br>
'''συμφόρημα, -ήματος (nom commun) (n)''' : .<br>
'''συμφόρησις, -ήσεως (nom commun) (f)''' : congestion.<br>
'''συμφορή, -ῆς (nom commun) (f)''' : Forme ionienne de ''συμφορά''.<br>
'''συμφέρω (verbe)''' : accumuler.<br>
'''συμφύρομαι (verbe)''' : .<br>
'''σύμφυσις, -εως (nom commun) (f)''' : symphyse.<br>
'''σύμφωνον, -ώνου (nom commun) (n)''' : consonne.<br>
'''συναγωγή, -ῆς (nom commun) (f)''' : Rassemblement, assemblée de gens. Rassemblement, regroupement, mis en tas, etc., de choses.<br>
'''συνάγω (verbe)''' : rassembler.<br>
'''συναίσθημα, -ήματος (nom commun) (n)''' : émotion.<br>
'''συναισθάνομαι (verbe)''' : .<br>
'''σύν (préposition)''' : Avec, à côté de.<br>
'''σύν- (préfixe)''' (Devient ''σύγ-'' devant ''γ'', ''κ'', ''ξ'', ''χ'' ; ''σύλ-'' devant ''λ'' ; ''σύμ-'' devant ''β'', ''π'', ''φ'', ''μ'', ''ψ''.) : syn-.<br>
'''σύναψις, -άψεως (nom commun) (f)''' : connexion.<br>
'''συνάπτω (verbe)''' : connecter.<br>
'''συνέδριον, -ίου (nom commun) (n)''' : conseil, congrégation.<br>
'''σύνεδρος, -έδρου (nom commun) (n)''' : conseiller, congrégationniste.<br>
'''συνείδησις, -ήσεως (nom commun) (f)''' : conscience.<br>
'''συνεργάτης, -ου (nom commun) (m)''' : collaborateur.<br>
'''σύνεσις, -έσεως (nom commun) (f)''' : connexion.<br>
'''συνέχεια, -ίας (nom commun) (f)''' : continuité.<br>
'''συνεχής, -ής, -ές (adjectif)''' : continu.<br>
'''συνεχέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''συνεχής''.<br>
'''συνεχέστερος, -έρα, -έστερον (adjectif)''' : Comparatif de ''συνεχής''.<br>
'''συνεχότατα, -, - (adverbe)''' : Superlatif de ''συνεχῶς''.<br>
'''συνεχότερον, -, - (adverbe)''' : Comparatif de ''συνεχῶς''.<br>
'''συνεχῶς (adverbe)''' : continuellement.<br>
'''συνίημι (verbe)''' : rejoindre.<br>
'''συνίστημι (verbe)''' : .<br>
'''συνουσία, -ας (nom commun) (f)''' : copulation.<br>
'''συνουσιάζω (verbe)''' : copuler.<br>
'''σύνοψις, -όψεως (nom commun) (f)''' : Vue d'ensemble. Coup d'œil général. Table des matières. (Figuré) Examen.<br>
'''σύνταγμα, -άγματος (nom commun) (n)''' : constitution.<br>
'''συνταγματικός, -ή, -όν (adjectif)''' : constitutionnel.<br>
'''συνταγματικότατα, -, - (adverbe)''' : Superlatif de ''συνταγματικῶς''.<br>
'''συνταγματικότερον, -, - (adverbe)''' : Comparatif de ''συνταγματικῶς''.<br>
'''συνταγματικότης, -τος (nom commun) (f)''' : constitutionnalité.<br>
'''συνταγματικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''συνταγματικός''.<br>
'''συνταγματικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''συνταγματικός''.<br>
'''συνταγματικῶς (adverbe)''' : constitutionnellement.<br>
'''σύνταξις, -άξεως (nom commun) (f)''' : mise en ordre.<br>
'''συντάσσω (verbe)''' : mettre en ordre.<br>
'''συνύπαρξις, -άρξεως (nom commun) (f)''' : coexistence.<br>
'''συνυπάρχω (verbe)''' : coexister.<br>
'''συνωμοσία, -ας (nom commun) (f)''' : conspiration.<br>
'''συνωμότης, -ου (nom commun) (m)''' : conspirateur.<br>
'''συνωμότρια, -ας (nom commun) (f)''' : conspiratrice.<br>
'''συνωμοτῶ (verbe)''' : conspirer.<br>
'''σῦριγξ, -ύριγγος (nom commun) (f)''' : Roseau. (Musique) Flûte de Pan.<br>
'''συριστί (adverbe)''' : .<br>
'''σύρξ, -κός (nom commun) (f)''' : Forme éolienne de ''σάρξ''.<br>
'''σύρραξις, -άξεως (nom commun) (f)''' : .<br>
'''συρτός, -οῦ (nom commun) (m)''' : tiroir.<br>
'''συσκευή, -ῆς (nom commun) (f)''' : appareil.<br>
'''συσσώρευσις, -ύσεως (nom commun) (f)''' : accumulation.<br>
'''σύσταμα, -άματος (nom commun) (n)''' : Forme dorienne de ''σύστημα''.<br>
'''συστέλλω (verbe)''' : Resserrer, contracter. Réprimer.<br>
'''σύστημα, -ήματος (nom commun) (n)''' : Réunion en un unique corps.<br>
'''συστολή, -ῆς (nom commun) (f)''' : Resserrement, contraction. Répression.<br>
'''σύ (pronom personnel)''' : tu.<br>
'''σφαγεύς, -έως (nom commun) (m)''' : tueur.<br>
'''σφαγή, -ῆς (nom commun) (f)''' : abattage.<br>
'''σφάζω (verbe)''' : tuer, sacrifier.<br>
'''σφαῖρα, -ίρας (nom commun) (f)''' : Balle, ballon ; globe.<br>
'''σφάκελος, -έλου (nom commun) (m)''' : nécrose.<br>
'''σφάλλω (verbe)''' : Faire tomber. faire chuter ; renverser. Défaire, avoir le dessus. Avoir lieu (bien ou mal tomber). Tromper, abuser. (Au passif) Se tromper, fauter. Emballer, rouler.<br>
'''σφάλμα, -τος (nom commun) (n)''' : Chute, faux pas. Erreur. Perte.<br>
'''σφεδανός, -ή, -όν (adjectif)''' : .<br>
'''σφένδαμνος, -άμνου (nom commun) (f)''' : érable.<br>
'''σφενδόνη, -ης (nom commun) (f)''' : fronde.<br>
'''σφενδονήτης, -ου (nom commun) (m)''' : frondeur.<br>
'''σφενδονητικός, -ή, -όν (adjectif)''' : frondeur.<br>
'''σφενδονητικῶς (adverbe)''' : -ment.<br>
'''σφενδονητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σφενδονητικός''.<br>
'''σφενδονητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σφενδονητικός''.<br>
'''σφενδονητικώτατα, -, - (adverbe)''' : Superlatif de ''σφενδονητικῶς''.<br>
'''σφενδονητικώτερον, -, - (adverbe)''' : Comparatif de ''σφενδονητικῶς''.<br>
'''σφήν, -ός (nom commun) (m)''' : coin (instrument).<br>
'''σφηνάριον, -ίου (nom commun) (n)''' : .<br>
'''σφηνεύς, -έως (nom commun) (m)''' : coin.<br>
'''σφηνίσκος, -ου (nom commun) (m)''' : .<br>
'''σφηνοειδής, -ής, -ές (adjectif)''' : cunéiforme.<br>
'''σφηνοκέφαλος, -ος, -ον (adjectif)''' : .<br>
'''σφηνόπους, -δός (nom commun) (m)''' : .<br>
'''σφηνοπώγων, -ονος (nom commun) (m)''' : coin.<br>
'''σφηνῶ (verbe)''' : .<br>
'''σφήνωσις, -ώσεως (nom commun) (f)''' : .<br>
'''σφήξ, -ῆκος (nom commun) (f)''' : guêpe.<br>
'''σφίγγω (verbe)''' : attacher fortement.<br>
'''σφιγκτήρ, -ῆρος (nom commun) (m)''' : sphincter.<br>
'''σφιγμός, -οῦ (nom commun) (m)''' : .<br>
'''σφόγγος, -ου (nom commun) (m)''' : Forme attique de ''σπόγγος''.<br>
'''σφοδρός, -ά, -όν (adjectif)''' : véhément.<br>
'''σφοδρότατα, -, - (adverbe)''' : Superlatif de ''σφοδρῶς''.<br>
'''σφοδρότερον, -, - (adverbe)''' : Comparatif de ''σφοδρῶς''.<br>
'''σφοδρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''σφοδρός''.<br>
'''σφοδρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''σφοδρός''.<br>
'''σφοδρότης, -τος (nom commun) (f)''' : véhémence.<br>
'''σφοδρῶς (adverbe)''' : véhémentement.<br>
'''σφόνδυλος, -ύλου (nom commun) (m)''' : vertèbre.<br>
'''σφραγίς, -δος (nom commun) (f)''' : sceau.<br>
'''σφράγισμα, -ατος (nom commun) (n)''' : .<br>
'''σφραγισμός, -οῦ (nom commun) (m)''' : .<br>
'''σφραγιστήρ, -ῆρος (nom commun) (m)''' : .<br>
'''σφραγιστήριον, -ίου (nom commun) (n)''' : .<br>
'''σφραγιστής, -οῦ (nom commun) (m)''' : .<br>
'''σφραγιστός, -ή, -όν (adjectif)''' : .<br>
'''σφυγμός, -οῦ (nom commun) (m)''' : pouls.<br>
'''σφύζω (verbe)''' : .<br>
'''σφύξις, -εως (nom commun) (f)''' : palpitation.<br>
'''σφῦρα, -ύρας (nom commun) (f)''' : marteau.<br>
'''σφώ (pronom personnel)''' : vous (vous deux).<br>
'''σχεδίασμα, -τος (nom commun) (n)''' : caprice.<br>
'''σχῆμα, -ήματος (nom commun) (n)''' : Manière d'être. Forme, figure, extérieur. Apparence, faux-semblant.<br>
'''σχίζω (verbe)''' : Fendre, séparer en fendant. Séparer en douze parts, avec l’idée de violence. Déchirer la peau avec ses griffes. Fendre, séparer, partager en deux.<br>
'''σχίσμα, -ατος (nom commun) (n)''' : division.<br>
'''σχοῖνος, -ίνου (nom commun) (m)''' : corde.<br>
'''σχολαστικός, -ή, -όν (adjectif)''' : Désœuvré. Inoccupé ; studieux.<br>
'''σχολαστικός, -οῦ (nom commun) (m)''' : Homme d'étude. (Péjoratif) Homme d'étude détaché des réalités de la vie ; pédant, nigaud, etc.<br>
'''σχολεῖον, -ίου (nom commun) (n)''' : école.<br>
'''σχολή, -ῆς (nom commun) (f)''' : Repos. Temps libre. Philosophie, méditation. École.<br>
'''σχολιάζω (verbe)''' : commenter.<br>
'''σχολιαστής, -οῦ (nom commun) (m)''' : commentateur.<br>
'''σχολιάστρια, -ας (nom commun) (f)''' : commentatrice.<br>
'''σχολιογράφος, -ου (nom commun) (m/f)''' : chroniqueur.<br>
'''σχόλιον, -ίου (nom commun) (n)''' : commentaire.<br>
'''σῴζω (verbe)''' : sauver.<br>
'''σωλήν, -ῆνος (nom commun) (m)''' : Tube ; tuyau.<br>
'''σωληνοειδής -ής -ές (adjectif)''' : .<br>
'''σῶμα, -ώματος (nom commun) (n)''' : corps.<br>
'''σωματικός, -ή, -όν (adjectif)''' : corporel.<br>
'''σωματικῶς (adverbe)''' : corporellement.<br>
'''σωματικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''σωματικός''.<br>
'''σωματικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''σωματικός''.<br>
'''σωματικώτατα, -, - (adverbe)''' : Superlatif de ''σωματικῶς''.<br>
'''σωματικώτερον, -, - (adverbe)''' : Comparatif de ''σωματικῶς''.<br>
'''σωμάτιον, -ίου (nom commun) (n)''' : codex.<br>
'''σώος, -α, -ον (adjectif)''' : sain.<br>
'''σωτήρ, -ῆρος (nom commun) (m)''' : Sauveur ; libérateur.<br>
'''σωφρονιστήριον, -ίου (nom commun) (n)''' : .<br>
'''σωφρονιστήρ, -ῆρος (nom commun) (m)''' : .<br>
'''σωφρονίζω (verbe)''' : modérer, tempérer.<br>
'''σωφροσύνη, -ης (nom commun) (f)''' : modération, tempérence.<br>
'''σώφρων, -ων, -ῶφρον (adjectif)''' : prudent.<br>
'''σϝάδυς, -εια, -υ (adjectif)''' : Forme ancienne de ''ἡδύς''.<br>
'''Σάϊς, -εως (nom commun) (f)''' : Saïs.<br>
'''Σαλμακίς, - (nom commun) (f)''' : Salmacis.<br>
'''Σαλομών, -ος (nom propre) (m)''' : Salomon.<br>
'''Σαλμωνεύς, -έως (nom propre) (m)''' : Salmonée.<br>
'''Σαμάρεια, -ίας (nom propre) (f)''' : Samarie.<br>
'''Σαμοθρᾴκη, -ης (nom propre) (f)''' : Samothrace.<br>
'''Σαμόθρᾳξ, -κος (nom commun) (m)''' : Samothracien.<br>
'''Σάμος, -ου (nom propre) (f)''' : Samos.<br>
'''Σανδρόκυπτος, -ύπτου (nom propre) (m)''' : Chandragupta.<br>
'''Σαγχουνιάθων, - (nom propre) (m)''' : Sanchoniathon.<br>
'''Σαούλ (nom propre) (m)''' : Saül.<br>
'''Σαπφώ, -οῦς (nom propre) (f)''' : Sapphô.<br>
'''Σαπώρης, -ου (nom commun) (m)''' : Shapur.<br>
'''Σάρα, -ας (prénom) (f)''' : Sarah.<br>
'''Σαρδόνιος, -ίου (nom commun) (m)''' : Sarde.<br>
'''Σαρδώ, -οῦς (nom propre) (f)''' : Sardaigne.<br>
'''Σαυρομάτης, -ου (nom commun) (m)''' : Sarmate.<br>
'''Σαυροματία, -ας (nom propre) (f)''' : Sarmatie.<br>
'''Σαυρομάτις, -δος (nom commun) (f)''' : Sarmate.<br>
'''Σατάν (nom propre) (m)''' : Satan.<br>
'''Σδεύς, -έως (nom propre) (f)''' : Autre forme éolienne de ''Ζεύς''.<br>
'''Σεϐάστεια, -ίας (nom propre) (f)''' : Sivas.<br>
'''Σεϐαστή, -ῆς (nom propre) (f)''' : .<br>
'''Σεϐαστός, -οῦ (nom propre) (m)''' : Sébastien.<br>
'''Σειρήν, -ῆνος (nom propre) (f)''' : sirène.<br>
'''Σείριος, -ίου (nom propre) (m)''' : Sirius.<br>
'''Σελεύκεια, -ίας (nom propre) (f)''' : Séleucie.<br>
'''Σελεύκειος, -α, -ον (adjectif)''' : séleucien.<br>
'''Σελευκεύς, -έως (nom commun) (m)''' : Séleucien.<br>
'''Σελευκίδης, -ου (nom commun) (m)''' : Séleucide.<br>
'''Σελευκίς, -δος (nom commun) (f)''' : Séleucienne.<br>
'''Σέλευκος, -ύκου (nom propre) (m)''' : Séleuce.<br>
'''Σελήνη, -ης (nom propre) (f)''' : [[wikt:Séléné|Séléné]].<br>
'''Σεντικλῆς, -έους (nom propre) (m)''' : Senticlès.<br>
'''Σέργιος, -ίου (nom propre) (m)''' : Serge.<br>
'''Σέσορθος, -όρθου (nom commun) (m)''' : Djéser.<br>
'''Σεύθης, -ου (nom commun) (m)''' : Seuthès.<br>
'''Σευθόπολις, -όλεως (nom propre) (f)''' : .<br>
'''Σῆθ (nom propre) (m)''' : Seth.<br>
'''Σηρική, -ῆς (nom propre) (f)''' : Chine.<br>
'''Σθεννώ, -οῦς (nom propre) (f)''' : Sthéno.<br>
'''Σίϐυλλα, -ύλλης (nom propre) (f)''' : Sibylle.<br>
'''Σιδών, -ῶνος (nom propre) (f)''' : Sidon.<br>
'''Σικελία, -ας (nom propre) (f)''' : Sicile.<br>
'''Σικελιώτης, -ου (nom commun) (m)''' : Sicéliote.<br>
'''Σικελιῶτις, -ώτιδος (nom commun) (f)''' : Sicéliotide.<br>
'''Σίκελος, -έλου (nom commun) (m)''' : Sicule.<br>
'''Σικυών, -ῶνος (nom propre) (f)''' : Sicyone.<br>
'''Σίσυφος, -ύφου (nom propre) (m)''' : Sisyphe.<br>
'''Σίνη, -ης (nom propre) (f)''' : Chine.<br>
'''Σíνων, -ος (nom propre) (m)''' : Sinon. (cousin d'Ulysse)<br>
'''Σιός, -οῦ (nom propre) (m)''' : Autre forme béotienne de ''Ζεύς''.<br>
'''Σίσυφος, -ύφου (nom propre) (m)''' : Sisyphe.<br>
'''Σκορπιός, -οῦ (nom commun) (m)''' : Scorpion.<br>
'''Σκύθαινα, -ίνης (nom propre) (f)''' : Scythe.<br>
'''Σκύθης, -ου (nom propre) (m)''' : Scythe.<br>
'''Σκυθία, -ας (nom propre) (f)''' : Scythie.<br>
'''Σκύλλα, -ης (nom propre) (f)''' : Scylla.<br>
'''Σκύλλη, -ης (nom propre) (f)''' : Forme homérique de ''Σκύλλα''.<br>
'''Σόλλαξ, -κος (nom propre) (m)''' : Sollax (ancien nom du Tigre).<br>
'''Σολομών, -ος (nom propre) (m)''' : Salomon.<br>
'''Σόλων, -ος (nom propre) (m)''' : Solon. (cousin d’Ulysse)<br>
'''Σούηϐος, -ήϐου (nom propre) (m)''' : Suève.<br>
'''Σουοϐηνός, -οῦ (nom commun) (m)''' : Slave.<br>
'''Σουσάννα, -ας (nom propre) (f)''' : Suzanne.<br>
'''Σοῦφις, -ύφιδος (nom propre) (m)''' : Souphis.<br>
'''Σοῦχος, -ύχου (nom propre) (m)''' : Sobek.<br>
'''Σοφία, -ας (nom propre) (f)''' : Sophie.<br>
'''Σοφοκλῆς, -έους (nom propre) (m)''' : Sophocle.<br>
'''Σπάρτα, -ας (nom propre) (f)''' : Forme dorienne de ''Σπάρτη''.<br>
'''Σπάρτη, -ης (nom propre) (f)''' : Sparte.<br>
'''Σπαρτιάτης, -ου (nom commun) (m)''' : Spartiate.<br>
'''Σπαρτιᾶτις, -άτιδος (nom commun) (f)''' : Spartiate.<br>
'''Σταυανός, -οῦ (nom commun) (m)''' : Slave.<br>
'''Στέφανος, -άνου (nom propre) (m)''' : Étienne ; Stéphane.<br>
'''Στράϐων, -ος (nom propre) (m)''' : Strabon.<br>
'''Στρογγύλη, ης (nom propre) (f)''' : Stromboli. (île)<br>
'''Στρούθας, -ου (nom propre) (m)''' : Struthas.<br>
'''Στύξ, -γός (nom propre) (f)''' : Styx.<br>
'''Σύϐαρις, -άριδος (nom propre) (f)''' : Sybaris (ville).<br>
'''Σύϐαρις, -άρεως (nom propre) (m)''' : Sybaris (fleuve).<br>
'''Συδύκ (nom propre) (m)''' : Sydyk.<br>
'''Συρία, -ας (nom propre) (f)''' : Syrie.<br>
'''Συριακός, -ός, -όν (adjectif)''' : syrien.<br>
'''Σύριος, -ίου (nom commun) (m)''' : Syrien.<br>
'''Σῦριγξ, -ύριγγος (nom propre) (f)''' : Syrinx.<br>
'''Σφίγξ, -γός (nom propre) (f)''' : Sphinx, Sphinge.<br>
'''Σωσίας, -ου (nom propre) (m)''' : Sosie.<br>
'''Σωσίπατρος, -άτρου (nom propre) (m)''' : Sosipatros.<br>
==Τ==
'''τάγηνον, -ήνου (nom commun) (n)''' : Forme dorienne de ''τήγανον''.<br>
'''τάγμα, -τος (nom commun) (n)''' : Arrangement. (Militaire) Régiment.<br>
'''ταινία, -ας (nom commun) (f)''' : ruban.<br>
'''ταινίη, -ης (nom commun) (f)''' : Forme ionienne de ''ταινία''.<br>
'''τακτική, -ῆς (nom commun) (f)''' : tactique.<br>
'''τακτικός, -ή, -όν (adjectif)''' : tactique.<br>
'''τακτικῶς (adverbe)''' : tactiquement.<br>
'''τακτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''τακτικός''.<br>
'''τακ τικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''τακτικός''.<br>
'''τακτικώτατα, -, - (adverbe)''' : Superlatif de ''τακτικῶς''.<br>
'''τακτικώτερον, -, - (adverbe)''' : Comparatif de ''τακτικῶς''.<br>
'''τάλαντον, -άντου (nom commun) (n)''' : plateau de balance.<br>
'''ταλαντοῦμαι (verbe)''' : osciller.<br>
'''ταλάντωσις, -ώσεως (nom commun) (f)''' : oscillation.<br>
'''ταμεῖον, -ίου (nom commun) (n)''' : .<br>
'''ταμία, -ας (nom commun) (f)''' : maîtresse de maison.<br>
'''ταμιακόν, -οῦ (nom commun) (m)''' : fisc.<br>
'''ταμιακός, -ή, -όν (adjectif)''' : fiscal.<br>
'''ταμιακότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ταμιακός''.<br>
'''ταμιακότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ταμιακός''.<br>
'''ταμιακῶς (adverbe)''' : fiscalement.<br>
'''ταμίας, -ου (nom commun) (m)''' : Dispensateur, distributeur ; partageur. Intendant, économe. Gardien d’un trésor. Directeur, ordonnateur ; arbitre.<br>
'''ταμιεῖον, -ίου (nom commun) (n)''' : caisse.<br>
'''ταμιεύω (verbe)''' : .<br>
'''τάμνω (verbe)''' : Forme homérique et ionienne de ''τέμνω''.<br>
'''τάξις, -εως (nom commun) (f)''' : disposition.<br>
'''τάξος, -ου (nom commun) (f)''' : if.<br>
'''τάπης, -τος (nom commun) (m)''' : tapis.<br>
'''τάραγμα, -άγματος (nom commun) (m)''' : inquiétude ; trouble.<br>
'''ταραξίας, -ου (nom commun) (m)''' : trouble-fête.<br>
'''τάραξις, -άξεως (nom commun) (f)''' : trouble.<br>
'''ταράσσω (verbe)''' : troubler.<br>
'''ταραχή, -ῆς (nom commun) (f)''' : trouble.<br>
'''ταρταροῦχος, -ος, -ον (adjectif)''' : du Tartare.<br>
'''τάσις, -εως (nom commun) (f)''' : tension.<br>
'''τάσσω (verbe)''' : disposer.<br>
'''τατᾶ (interjection)''' : papa ; maman.<br>
'''ταταλίζω (verbe)''' : .<br>
'''ταῦρος, -ύρου (nom commun) (m)''' : taureau.<br>
'''ταυρῶ (verbe)''' : tirer.<br>
'''ταῦ (nom commun) (n)''' : tau.<br>
'''τάφος, -ου (nom commun) (m)''' : tombeau.<br>
'''τάχα (adverbe)''' (Devient ''τάχ’'' devant un mot commençant par une voyelle à esprit doux.) : bientôt.<br>
'''ταχέως (adverbe)''' : rapidement.<br>
'''ταχύς, -εῖα, -ύ (adjectif)''' : rapide ; pressé.<br>
'''ταχύτατος, -άτη, -ύτατον (adjectif)''' : Superlatif de ''ταχύς''.<br>
'''ταχύτερος, -έρα, -ύτερον (adjectif)''' : Comparatif de ''ταχύς''.<br>
'''ταχυτής, -ῆτος (nom commun) (f)''' : rapidité.<br>
'''ταώς, -ώ (nom commun) (m)''' : paon.<br>
'''τείνω (verbe)''' : étirer ; tendre.<br>
'''τέκτων, -ονος (nom commun) (m)''' : Auteur, créateur. Ouvrier, artisan.<br>
'''τεῖχος, -ίχους (nom commun) (n)''' : mur de ville.<br>
'''τέλειος, -ία, -ιον (adjectif)''' : terminé ; parfait.<br>
'''τελειότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''τέλειος''.<br>
'''τελειότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''τέλειος''.<br>
'''τελείως (adverbe)''' : parfaitement.<br>
'''τελειῶ (verbe)''' : terminer.<br>
'''τέλεσμα, -έσματος (nom commun) (n)''' : Paiement, taxe. Certificat.<br>
'''τελεστιχίς, -δος (nom commun) (f)''' : téléstiche.<br>
'''τελετή, -ῆς (nom commun) (f)''' : cérémonie.<br>
'''τελευταῖος, -ία, -ῖον (adjectif)''' : dernier.<br>
'''τελευτή, -ῆς (nom commun) (f)''' : fin ; finalité.<br>
'''τελέω (verbe)''' : Accomplir. Mettre un terme à.<br>
'''τέλος, -ους (nom commun) (n)''' : Achèvement ; accomplissement ; réalisation. Prix dans les luttes.<br>
'''τέμαχος, -άχους (nom commun) (n)''' : tranche ; morceau.<br>
'''τέμενος, -ένους (nom commun) (n)''' : téménos.<br>
'''τέμνω (verbe)''' : couper.<br>
'''τέρας, -τος (nom commun) (n)''' : Signe divin ; monstre.<br>
'''τεράστιος -α -ον (adjectif)''' : monstrueux.<br>
'''τερπνός, -ή, -όν (adjectif)''' : Amusant ; plaisant.<br>
'''τερπνότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''τερπνός''.<br>
'''τερπνότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''τερπνός''.<br>
'''τερπνότης, -τος (nom commun) (f)''' : amusement.<br>
'''τερπνῶς (adverbe)''' : plaisamment.<br>
'''τέρπω (adverbe)''' : Prendre plaisir ; s’amuser.<br>
'''τέσσαρες (adjectif numéral)''' : quatre.<br>
'''τετράδιον, -ίου (nom commun) (n)''' : cahier.<br>
'''τετραίνω (verbe)''' : trouer.<br>
'''τετράλημμα, -ήμματος (nom commun) (n)''' : tétralemme.<br>
'''τετράπους, -δος (nom commun) (m)''' : quadrupède.<br>
'''τετράς, -δος (nom commun) (f)''' : .<br>
'''τετράων, -ος (nom commun) (m)''' : coq de bruyère.<br>
'''τετράστιχον, -ίχου (nom commun) (n)''' : quatrain.<br>
'''τετταράκοντα (adjectif numéral)''' quarante.<br>
'''τέττιξ, -γος (nom commun) (m)''' : cigale.<br>
'''τεῦτλον, -ύτλου (nom commun) (n)'''' : blette.<br>
'''τεῦχος, -ύχους (nom commun) (n)''' : Ustensile, instrument. (Au pluriel) Armes, armure. (Au pluriel) Agrès de navire (voiles, cordages, rames). Urne pour les libations. Urne funéraire. Baignoire. Tonneau de bois. Huche pour la farine. Ruche d’abeilles.(Par analogie) Vaisseau du corps. Enveloppe qui enferme les petits. Livre.<br>
'''τεύχω (verbe)''' : .<br>
'''τέφρα, -ας (nom commun) (f)''' : cendre.<br>
'''τέφρη, -ης (nom commun) (f)''' : Forme homérique et ionienne de ''τέφρα''.<br>
'''τεχνάζω (verbe)''' : faire avec art.<br>
'''τέχνασμα, -άσματος (nom commun) (n)'''' : Artifice, machination, ruse.<br>
'''τέχνη, -ης (nom commun) (f)''' : Art ; habileté.<br>
'''τέχνημα, -ήματος (nom commun) (n)''' : artéfact.<br>
'''τεχνητός, -ή, -όν (adjectif)''' : artificiel.<br>
'''τεχνητῶς (adverbe)''' : artificiellement.<br>
'''τεχνητώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''τεχνητός''.<br>
'''τεχνητώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''τεχνητός''.<br>
'''τεχνικός, -ή, -όν (adjectif)''' : artistique.<br>
'''τεχνικῶς (adverbe)''' : artistiquement.<br>
'''τεχνικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''τεχνικός''.<br>
'''τεχνικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''τεχνικός''.<br>
'''τεχνῶμαι (verbe)''' : .<br>
'''τηγάνιον, -ίου (nom commun) (n)''' : Diminutif de ''τήγανον''.<br>
'''τήγανον, -άνου (nom commun) (n)''' : poêle à frire.<br>
'''τήκω (verbe)''' : fondre.<br>
'''τῆλε (adverbe ; préposition)''' Loin, au loin. (Avec le génitif) Loin de.<br>
'''τήμερον (adverbe)''' : Forme attique de ''σήμερον''.<br>
'''-τήρ, -ῆρος (suffixe) (m)''' : Suffixe nominal.<br>
'''-τής, -ῆτος (suffixe) (f)''' : Suffixe permettant de créer à partir d’un adjectif le nom désignant la qualité correspondante.<br>
'''-της, -τος (suffixe) (f)''' : Suffixe de même usage que ''-τής''.<br>
'''τίγρις, -εως (nom commun) (m/f)''' : Tigre ; tigresse.<br>
'''τίθημι (verbe)''' : Poser ; placer.<br>
'''τιθήνη, -ης (nom commun) (f)''' : nourrice.<br>
'''τεκμηρίωσις, -ώσεως (nom commun) (f)''' : documentation.<br>
'''τεκμηριῶ (verbe)''' : documenter.<br>
'''τίκτω (verbe)''' : Engendrer, produire ; mettre au monde.<br>
'''τιμή, -ῆς (nom commun) (f)''' : (Sens positif) Évaluation, estimation. Prix attaché à un honneur. Ce qui est tenu en honneur ; objet de l’estime, du respect ; autorité, magistrature. (Sens négatif) Peine, châtiment, vengeance.<br>
'''τίμιος, -α, -ον (adjectif)''' : honnête.<br>
'''τιμιότης, -τος (nom commun) (f)''' : honnêteté.<br>
'''τιμωρητέος, -α, -ον (adjectif)''' : punissable.<br>
'''τιμωρητικός, -ή, -όν (adjectif)''' : punitif.<br>
'''τιμωρητικῶς (adverbe)''' : punitivement.<br>
'''τιμωρητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''τιμωρητικός''.<br>
'''τιμωρητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''τιμωρητικός''.<br>
'''τιμωρητικώτατα, -, - (adverbe)''' : Superlatif de ''τιμωρητικῶς''.<br>
'''τιμωρητικώτερον, -, - (adverbe)''' : Comparatif de ''τιμωρητικῶς''.<br>
'''τιμωρία, -ας (nom commun) (f)''' : punition.<br>
'''τιμωρῶ (verbe)''' : punir.<br>
'''τιμῶ (verbe)''' : honorer.<br>
'''τίνω (verbe)''' : Payer ; payer pour ses fautes, expier.<br>
'''τιούχα, -ας (nom commun) (f)''' : Autre forme béotienne de ''τύχη''.<br>
'''τίσις, -εως (nom commun) (f)''' : Paiement, récompense. Pénalité, vengeance.<br>
'''τιταίνω (verbe)''' : étendre.<br>
'''τίταξ, -κos (nom commun) (m)''' : roi.<br>
'''τίτας, -αντος (nom commun) (m)''' : vengeur.<br>
'''τίτης, -ου (nom commun) (m)''' : Forme dorienne de ''τίτας''.<br>
'''τῖφος, -ίφου (nom commun) (m)''' : Étang, marais.<br>
'''τίω (verbe)''' : rendre (i.e. payer) des hommages à quelqu’un, estimer une personne. Évaluer, estimer.<br>
'''τμῆμα, -ήµατος (nom commun) (n)''' : Partie, secteur ; section.<br>
'''τμῆσις, -ήσεως (nom commun) (f)''' : césure.<br>
'''τοιοῦτος, -αύτη, -οῦτο (pronom)''' : .<br>
'''τοῖχος, -ίχου (nom commun) (m)''' : Mur de maison ; bord ou paroi d’un navire.<br>
'''τοιχωρύχημα, -ήµατος (nom commun) (n)''' : cambriolage.<br>
'''τοιχώρυχος, -ύχου (nom commun) (m)''' : cambrioleur.<br>
'''τοιχωρυχῶ (verbe)''' : cambrioler.<br>
'''τοκογλυφία, -ας (nom commun) (f)''' : usure.<br>
'''τοκογλυφικός, -ή, -όν (adjectif)''' : usurier.<br>
'''τοκογλύφος, -ου (nom commun) (m)''' : usurier.<br>
'''τόκος, -ου (nom commun) (m)''' : Parturition, accouchement. Descendance. Intérêt de l'argent prêté.<br>
'''τόλμα, -ης (nom commun) (f)''' : Audace ; courage.<br>
'''τολμηρός, -ή, -όν (adjectif)''' : hardi.<br>
'''τολμηρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''τολμηρός''.<br>
'''τολμηρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''τολμηρός''.<br>
'''τολμηρότης, -τος (nom commun) (f)''' : hardiesse.<br>
'''τολμηρῶς (adverbe)''' : hardiment.<br>
'''τολμηρώτατα, -, - (adverbe)''' : Superlatif de ''τολμηρῶς''.<br>
'''τολμηρώτερον, -, - (adverbe)''' : Comparatif de ''τολμηρῶς''.<br>
'''τολμῶ (verbe)''' : oser.<br>
'''-τομία, -ας (suffixe) (f)''' : Suffixe signifiant « coupure », « césure ».<br>
'''τόμος, -ου (nom commun) (m)''' : Tranche, pièce ; chose coupée.<br>
'''τομός, -οῦ (nom commun) (m)''' : Coupure, action de couper.<br>
'''τοπικός, -ή, -όν (adjectif)''' : local.<br>
'''τοπικῶς (adverbe)''' : localement.<br>
'''τοπικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''τοπικός''.<br>
'''τοπικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''τοπικός''.<br>
'''τοπικώτατα, -, - (adverbe)''' : Superlatif de ''τοπικῶς''.<br>
'''τοπικώτερον, -, - (adverbe)''' : Comparatif de ''τοπικῶς''.<br>
'''τοξάριον, -ίου (nom commun) (n)''' : archet.<br>
'''τοξικός, -ή, -όν (adjectif)''' : qui convient pour un arc ou pour des flèches.<br>
'''τόξον, -ου (nom commun) (n)''' : arc ; arc-en-ciel.<br>
'''τοξότης, -ου (nom commun) (m)''' : archer.<br>
'''τὸ (article défini)''' : le (neutre).<br>
'''τούχα, -ας (nom commun) (f)''' : Forme béotienne de ''τύχη''.<br>
'''τράγημα, -ατος (nom commun) (n)''' : friandise.<br>
'''τραγικός, -ή, -όν (adjectif)''' : tragique.<br>
'''τραγικότατα, -, - (adverbe)''' : Superlatif de ''τραγικῶς''.<br>
'''τραγικότερον, -, - (adverbe)''' : Comparatif de ''τραγικῶς''.<br>
'''τραγικῶς (adverbe)''' : comiquement.<br>
'''τραγικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''τραγικός''.<br>
'''τραγικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''τραγικός''.<br>
'''τραγίσκος, -ου (nom commun) (m)''' : chevreau.<br>
'''τράγος, -ου (nom commun) (m)''' : bouc.<br>
'''τραγῳδία, -ας (nom commun) (f)''' : tragédie.<br>
'''τραγωδῶ (verbe)''' : chanter.<br>
'''τράπεζα, -ης (nom commun) (f)''' : table.<br>
'''τραυλίζω (verbe)''' : bégayer, bafouiller.<br>
'''τραυλισμός, -οῦ (nom commun) (m)''' : bégaiement, bafouillis.<br>
'''τραυλός, -οῦ (nom commun) (m)''' : bègue, bafouilleur.<br>
'''τραῦμα, -ύματος (nom commun) (n)''' : Blessure. Déroute, désastre.<br>
'''τράχηλος, -ήλου (nom commun) (m)''' : Cou. (En particulier) Derrière du cou, nuque.<br>
'''τραχύς, -εῖα, -ύ (adjectif)''' : rude.<br>
'''τράχω (verbe)''' : Forme dorienne de ''τρέχω''.<br>
'''τρεῖς (adjectif numéral)''' : trois.<br>
'''τρέμω (verbe)''' : trembler, s’agiter ; s’ébranler.<br>
'''τρέπω (verbe)''' : tourner.<br>
'''τρέστης, -ου (nom commun) (m)''' : couard ; peureux.<br>
'''τρέφω (verbe)''' : Rendre compact. Rendre gras, engraisser, nourrir. Nourrir, élever. (Par extension) Élever, former, façonner, instruire. Pourvoir aux besoins de. S’épaissir, se condenser. Être nourri ou élevé.<br>
'''τρέχω (verbe)''' : courir.<br>
'''τρέω (verbe)''' : avoir peur.<br>
'''τρῆμα, -ήµατος (nom commun) (n)''' : trou.<br>
'''τρηρός, -ή, -όν (adjectif)''' : fou.<br>
'''τρήρων, -ος (nom commun) (m/f)''' : timide.<br>
'''τριάκοντα (adjectif numéral)''' : trente.<br>
'''τρίϐω (verbe)''' : frotter.<br>
'''τρίλημμα, -ήμματος (nom commun) (n)''' : trilemme.<br>
'''τρίμμα, -τος (nom commun) (n)''' : .<br>
'''τρίπος, -ου (nom commun) (m)''' : trépied.<br>
'''τρίπους, -δος (adjectif)''' : tripède.<br>
'''τρισκελής, -οῦ (nom commun) (m)''' : triskèle.<br>
'''τρισκέλιον, -ίου (nom commun) (n)''' : Diminutif de ''τρισκελής''.<br>
'''τριταγωνιστής, -οῦ (nom commun) (m)''' : tritagoniste.<br>
'''τρίτος, -η, -ον (adjectif numéral)''' : troisième.<br>
'''τρίφυλλον, -ύλλου (nom commun) (n)''' : trèfle.<br>
'''-τρον, -ου (suffixe) (n)''' : Suffixe servant à former des noms d’instruments.<br>
'''τρόπαιον, -ίου (nom commun) (n)''' : trophée.<br>
'''τροπή, -ῆς (nom commun) (f)''' : Tour. Fuite. Révolution, changement. (Rhétorique) Tournure de phrase.<br>
'''τροπικός, -ή, -όν (adjectif)''' : tournant.<br>
'''τρόπος, -ου (nom commun) (m)''' : Tour, direction ; façon, mode, manière.<br>
'''τροχαῖος, -ίου (nom commun) (m)''' : trochée.<br>
'''τροχαλία, -ας (nom commun) (f)''' : poulie.<br>
'''τροχίλος, -ου (nom commun) (m)''' : poulie.<br>
'''τρόχος, -ου (nom commun) (m)''' : blaireau.<br>
'''τροχός, -οῦ (nom commun) (m)''' : Roue. Tour de potier. Pain (de forme ronde) de suif ou de cire.<br>
'''τρύϐλιον, -ίου (nom commun) (n)''' : bol ; coupe.<br>
'''τρυγόνιον, -ίου (nom commun) (n)''' : Diminutif de ''τρυγών ''.<br>
'''τρυγών, -όνος (nom commun) (f)''' : tourterelle.<br>
'''τρύζω (verbe)''' : .<br>
'''τρῦπα, -ύπας (nom commun) (f)''' : trou.<br>
'''τρύπημα, -ήματος (nom commun) (n)''' : trou.<br>
'''τρύω (verbe)''' : user.<br>
'''τύρρις, -ος (nom commun) (f)''' : variante de ''τύρσις''.<br>
'''τύρσις, -ος (nom commun) (f)''' : tour (construction élevée).<br>
'''τρύφαξ, -κος (nom commun) (m)''' : débauché.<br>
'''τρυφερός, -ά, -όν (adjectif)''' : délicat.<br>
'''τρυφερότης, -τος (nom commun) (f)''' : délicatesse.<br>
'''τρυφερώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''τρυφερός''.<br>
'''τρυφερώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''τρυφερός''.<br>
'''τρυφερῶς (adverbe)''' : délicatement.<br>
'''τρυφή, -ῆς (nom commun) (f)''' : Douceur, mollesse. Luxe, délicatesse. Dissolution, débauche.<br>
'''τρύφημα, -ήματος (nom commun) (n)''' : orgueil.<br>
'''τρυφητής, -ής, -ές (adjectif)''' : voluptueux.<br>
'''τρυφῶ (verbe)''' : être extravagant, se donner des airs.<br>
'''τρῦχος, -ύχους (nom commun) (n)''' : Haillon, lambeau.<br>
'''τρύχω (verbe)''' : épuiser, user.<br>
'''τρώγω (verbe)''' : ronger, grignoter.<br>
'''τρωϊκός, -ή -όν (adjectif)''' : troyen.<br>
'''τρῶ (verbe)''' : avoir peur.<br>
'''τῦκον, -ύκου (nom commun) (m)''' : Forme béotienne de ''σῦκον''.<br>
'''τύμϐος, -ου (nom commun) (m)''' : tumulus.<br>
'''τυνδάρειος, -ος, -ον (nom commun) (m)''' : tyndaréen.<br>
'''τυπικός, -ή -όν (adjectif)''' : figuré.<br>
'''τυπικῶς (adverbe)''' : figurativement.<br>
'''τυπικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''τυπικός''.<br>
'''τυπικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''τυπικός''.<br>
'''τυπικώτατα, -, - (adverbe)''' : Superlatif de ''τυπικῶς''.<br>
'''τυπικώτερον, -, - (adverbe)''' : Comparatif de ''τυπικῶς''.<br>
'''τύπος, -ου (nom commun) (m)''' : Coup ; frappe. Frappe, marque du coup ; sceau, impression.<br>
'''τύπτω (verbe)''' : frapper.<br>
'''τύραννος, -άννου (nom commun) (m)''' : Maître, dominateur. (Péjoratif) Tyran, dictateur ; despote.<br>
'''τύρϐη, -ης (nom commun) (f)''' : tumulte, désordre.<br>
'''τυρός, -οῦ (nom commun) (m)''' : fromage.<br>
'''τυτώ, -οῦς (nom commun) (f)''' : chouette.<br>
'''τύ (pronom personnel)''' : Forme dorienne de ''σύ''.<br>
'''τυφλός, -ή -όν (adjectif)''' : aveugle.<br>
'''τῦφος, -ύφου (nom commun) (m)''' : fumée, vapeur qui monte au cerveau ; orgueil.<br>
'''τῦφω (verbe)''' : enfumer.<br>
'''τυχαῖος, -ία, -ῖον (adjectif)''' : chanceux.<br>
'''τυχερῶς (adverbe)''' : .<br>
'''τύχη, -ης (nom commun) (f)''' : chance.<br>
'''τυχηρός, -ά, -όν, (adjectif)''' : chanceux.<br>
'''τύψις, -εως (nom commun) (f)''' : remords.<br>
'''Ταλθύϐιος, -ίου (nom propre) (m)''' : Talthybios.<br>
'''Τάλως, -ώ (nom propre) (m)''' : Talos.<br>
'''Τάμεσις, -έσεως (nom propre) (f)''' : Tamise.<br>
'''Τάναϊς, -άϊδος (nom propre) (m)''' : Tanaïs.<br>
'''Τάν, -ός (nom propre) (m)''' : Forme crétoise de ''Ζεύς''.<br>
'''Ταραντῖνος, -ίνου (nom commun) (m)''' : Tarentin.<br>
'''Τάρας, -αντος (nom propre) (m)''' : Tarente.<br>
'''Τάρταρος, -άρου (nom propre) (m)''' : Tartare.<br>
'''Ταῦρος, -ύρου (nom propre) (m)''' : Taureau.<br>
'''Τειρεσίας, -ου (nom propre) (m)''' : Tirésias.<br>
'''Τεΐσπης, -ου (nom propre) (m)''' : Teispès.<br>
'''Τελεύτας, -αντος (nom propre) (m)''' : Téleutas.<br>
'''Τελευτίας, -ου (nom propre) (m)''' : Téleutias.<br>
'''Τερψιχόρα, -ας (nom propre) (f)''' : Terpsichore.<br>
'''Τεύτων, -ονος (nom commun) (m)''' : Teuton.<br>
'''Τηθύς, -ος (nom propre) (f)''' : [[wikt:Téthys|Téthys]].<br>
'''Τηλέγονος, -όνου (nom propre) (m)''' : Télégonos.<br>
'''Τηλέμαχος, -άχου (nom propre) (m)''' : Télémaque.<br>
'''Τηρεύς, -έως (nom propre) (m)''' : Térée.<br>
'''Τίγρης, -τος (nom propre) (m)''' : Tigre (fleuve du Moyen-Orient).<br>
'''Τίγρις, -δος (nom propre) (f)''' : Forme alternative de ''Τίγρης''.<br>
'''Τιθωνός, -οῦ (nom propre) (m)''' : Tithon.<br>
'''Τιμασίθεος, -έου (nom propre) (m)''' : Timasithée.<br>
'''Τιμόθεος, -έου (nom propre) (m)''' : Timothée.<br>
'''Τίμων, -ωνος (nom propre) (m)''' : Timon.<br>
'''Τισαμενός, -οῦ (nom propre) (m)''' : Tisamène.<br>
'''Τίσανδρος, -άνδρου (nom propre) (m)''' : Tisandre.<br>
'''Τισίας, -ου (nom propre) (m)''' : Tisias.<br>
'''Τισικράτης, -ου (nom propre) (m)''' : Tisicrate.<br>
'''Τισιφόνη, -ης (nom propre) (f)''' : Tisiphone (une des Érynies).<br>
'''Τισσαφέρνης, -ου (nom propre) (m)''' : Tissapherne.<br>
'''Τιτάν, -ᾶνος (nom propre) (m)''' : Titan.<br>
'''Τιτανίς, -δος (nom propre) (f)''' : Titanide.<br>
'''Τιτυός, -οῦ (nom propre) (m)''' : Tityos.<br>
'''Τόσορθρος, -όρθου (nom commun) (m)''' : Djéser.<br>
'''Τοξότης, -ου (nom propre) (m)''' : Sagittaire.<br>
'''Τουρκία, -ας (nom propre) (f)''' : Turquie.<br>
'''Τοῦρκος, -ύρκου (nom propre) (m)''' : Turc.<br>
'''Τρίπολις, -όλεως (nom propre) (f)''' : Tripoli.<br>
'''Τρίτων, -ος (nom propre) (m)''' : Triton.<br>
'''Τροία, -ας (nom propre) (f)''' : Troie.<br>
'''Τρωάς, -δος (nom commun) (f)''' : Troyenne.<br>
'''Τρώς, -ός (nom commun) (m)''' : Troyen.<br>
'''Τῦϐι (nom propre) (m)''' : Tybi.<br>
'''Τυδεύς, -έως (nom propre) (m)''' : Tydée.<br>
'''Τυνδάρεως, -εω (nom propre) (m)''' : Tyndare.<br>
'''Τυνδαρίδης, -ου (nom propre) (m)''' : Tyndaride.<br>
'''Τυνδαρίς, -δος (nom propre) (f)''' : Tyndaride.<br>
'''Τυφάων, -ος (nom propre) (m)''' : Forme de ''Τυφῶν''.<br>
'''Τυφωεύς, -έως (nom propre) (m)''' : Forme de ''Τυφῶν''.<br>
'''Τυφώς, - (nom propre) (m)''' : Forme de ''Τυφῶν''.<br>
'''Τυφῶν, -ος (nom propre) (m)''' : Typhon.<br>
'''Τύχη, -ης (nom propre) (f)''' : [[wikt:Tyché|Tyché]].<br>
'''Τύχων, -ος (nom propre) (m)''' : Tychon.<br>
==Υ==
'''ὕαινα, -ίνης (nom commun) (f)''' : hyène.<br>
'''ὕαλος, -άλου (nom commun) (m)''' : verre. (matière)<br>
'''ὑϐριζω (verbe)''' : maltraiter, outrager.<br>
'''ὕϐρις, -εως (nom commun) (f)''' : démesure, violence ; excès, outrage.<br>
'''ὑγίεια, -ίας (nom commun) (f)''' : propreté.<br>
'''ὑγιεινή, -ῆς (nom commun) (f)''' : santé.<br>
'''ὑγιεινός, -ή, -όν (adjectif)''' : salubre.<br>
'''ὑγραίνω (verbe)''' : humidifier.<br>
'''ὑγρασία, -ας (nom commun) (f)''' : humidité.<br>
'''ὑγρός, -ά, -όν (adjectif)''' : humide.<br>
'''ὑγρότατα, -, - (adverbe)''' : Superlatif de ''ὑγρῶς''.<br>
'''ὑγρότερον, -, - (adverbe)''' : Comparatif de ''ὑγρῶς''.<br>
'''ὑγρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ὑγρός''.<br>
'''ὑγρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''ὑγρός''.<br>
'''ὑγρῶς (adverbe)''' : humidement.<br>
'''ὑδραγωγεῖον, -ίου (nom commun) (n)''' : aqueduc.<br>
'''ὑδράργυρος, -ύρου (nom commun) (m)''' : mercure.<br>
'''ὑδρόμελι, -τος (nom commun) (n)''' : Boisson à base d'eau et de miel.<br>
'''ὑδρο- (préfixe)''' : hydro-.<br>
'''ὕδωρ, -ατος (nom commun) (n)''' : Eau ; sueur.<br>
'''ὕενος, -ένου (nom commun) (m)''' : mercure.<br>
'''υἱός, -οῦ (nom commun) (m)''' : fils.<br>
'''ὕλη, -ης (nom commun) (f)''' : matière ; bois.<br>
'''ὑμεῖς (pronom personnel)''' : vous.<br>
'''ὑμέτερος, -έρα, -έτερον (adjectif possessif)''' : votre.<br>
'''ὑμήν, -ένος (nom commun) (m)''' : Membrane ; pellicule enveloppant les organes du corps.<br>
'''ὕμνος, -ου (nom commun) (m)''' : trame ; chant.<br>
'''ὑμνῳδία, -ας (nom commun) (f)''' : hymne.<br>
'''ὑμνῶ (verbe)''' : .<br>
'''ὔμοι (adverbe)''' : Forme éolienne de ''ὁμοῦ''.<br>
'''ὔμοιος, -, - (adjectif)''' : Forme éolienne de ''ὅμοιος''.<br>
'''ὑμοῖος, -, -ῖον (adjectif)''' : Forme arcado-chypriote de ''ὅμοιος''.<br>
'''ὑπαγόρευσις, -ύσεως (nom commun) (f)''' : dictée.<br>
'''ὑπάγω (verbe)''' : mener sous.<br>
'''ὑπακτικόν -οῦ (nom commun) (n)''' : laxatif.<br>
'''ὑπακτικός -ή -όν (adjectif)''' : laxatif.<br>
'''ὑπακοή, -ῆς (nom commun) (f)''' : obéissance.<br>
'''ὑπάκουος, -η, -ον (adjectif)''' : obéissant.<br>
'''ὑπακούω (verbe)''' : obéir.<br>
'''ὑπαρξιακός, -ή, -όν (adjectif)''' : existentiel.<br>
'''ὕπαρξις, -άρξεως (nom commun) (f)''' : existence.<br>
'''ὑπάρχω (verbe)''' : exister.<br>
'''ὑπεξαίρεσις, -έσεως (nom commun) (f)''' : malversation.<br>
'''ὑπεξαιρῶ (verbe)''' : .<br>
'''ὑπερκόσμιος, -α, -ο (adjectif)''' : .<br>
'''ὑπέρ (adverbe ; préposition)''' : au-dessus.<br>
'''ὑπερϐάλλω (verbe)''' : exagérer.<br>
'''ὑπερϐολή, -ῆς (nom commun) (f)''' : exagération.<br>
'''ὑπερϐολικός, -ή, -όν (adjectif)''' : excessif.<br>
'''ὑπερθετικός, -ή, -όν (adjectif)''' : superlatif.<br>
'''ὑπεριώδης, -ης, -ες (adjectif)''' : ultraviolet.<br>
'''ὑπεροπτικός, -ή, -όν (adjectif)''' : arrogant.<br>
'''ὑπέροπτος, -ος, -ον (adjectif)''' : .<br>
'''ὑπεροψία, -ας (nom commun) (f)''' : arrogance.<br>
'''ὑπέρυθρος, -η, -ον (adjectif)''' : infrarouge.<br>
'''ὑπερφυσικός, -ή, -όν (adjectif)''' : surnaturel.<br>
'''ὑπεύθυνος, -ος, -ον (adjectif)''' : responsable.<br>
'''ὑπευθυνότητα, -ας (nom commun) (f)''' : responsabilité.<br>
'''ὑπηρεσία, -ας (nom commun) (f)''' : service.<br>
'''ὑπηρέτης, -ου (nom commun) (m)''' : serviteur.<br>
'''ὑπηρετικός, -ή, -όν (adjectif)''' : de service.<br>
'''ὑπηρετικῶς (adverbe)''' : -ment.<br>
'''ὑπηρετικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ὑπηρετικός''.<br>
'''ὑπηρετικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ὑπηρετικός''.<br>
'''ὑπηρετικώτατα, -, - (adverbe)''' : Superlatif de ''ὑπηρετικῶς''.<br>
'''ὑπηρετικώτερον, -, - (adverbe)''' : Comparatif de ''ὑπηρετικῶς''.<br>
'''ὑπηρέτρια, -ας (nom commun) (f)''' : servante.<br>
'''ὑπηρετῶ (verbe)''' : servir.<br>
'''ὑπνολαλία, -ας (nom commun) (f)''' : somniloquie.<br>
'''ὑπνολαλῶ (verbe)''' : somniloquer.<br>
'''ὕπνωσις, -ώσεως (nom commun) (f)''' : hypnose.<br>
'''ὑπνωτικός, -ή, -όν (adjectif)''' : hypnotique.<br>
'''ὑπνῶ (adjectif)''' : hypnotiser.<br>
'''ὑπόϐαθρον, -άθρου (nom commun) (n)''' : arrière-plan.<br>
'''ὑπόγειον, -ίου (nom commun) (n)''' : cave.<br>
'''ὑπόδειγμα, -ίγματος (nom commun) (n)''' : exemple.<br>
'''ὑποδειγματικός, -ή, -όν (adjectif)''' : exemplaire.<br>
'''ὑποδείκνυμι (verbe)''' : .<br>
'''ὑπόδημα, -ήματος (nom commun) (n)''' : chaussure.<br>
'''ὑποδηματοποιεῖον, -ίου (nom commun) (n)''' : boutique du cordonnier.<br>
'''ὑποδηματοποιός, -οῦ (nom commun) (m)''' : cordonnier.<br>
'''ὑποζύγιον, -ίου (nom commun) (n)''' : attelage.<br>
'''ὑπόθεσις, -έσεως (nom commun) (f)''' : supposition.<br>
'''ὑποκινῶ (verbe)''' : .<br>
'''ὑποκορίζομαι (verbe)''' : parler comme un enfant.<br>
'''ὑποκόρισμα, -τος (nom commun) (n)''' : sobriquet.<br>
'''ὑποκοριστικός, -ή, -όν (adjectif)''' : caressant, propre à atténuer.<br>
'''ὑποκρίνομαι (verbe)''' : jouer une pièce.<br>
'''ὑπόκρισις, -ίσεως (nom commun) (f)''' : Réponse. Action de jouer un rôle, une pièce, une pantomime. Débit théâtral, déclamation. Feinte, faux-semblant.<br>
'''ὑποκριτής, -οῦ (nom commun) (m)''' : Donneur de réponse. Acteur, comédien.<br>
'''ὑπόκωφος, -η, -ον (adjectif)''' : sourd.<br>
'''ὑποκώφως (adverbe)''' : sourdement.<br>
'''ὑπολογίζομαι (verbe)''' : calculer.<br>
'''ὑπολογισμός, -οῦ (nom commun) (m)''' : calcul.<br>
'''ὑπομιμνήσκω (verbe)''' : .<br>
'''ὑπομονή, -ῆς (nom commun) (f)''' : .<br>
'''ὑπόμνημα, -ήματος (nom commun) (n)''' : .<br>
'''ὑπόνομος, -ου (nom commun) (m)''' : égout.<br>
'''ὑπόστασις, -άσεως (nom commun) (f)''' : .<br>
'''ὑποταγή, -ῆς (nom commun) (f)''' : Subordination, soumission.<br>
'''ὑποτάσσω (verbe)''' : Subordonner, soumettre. Dominer, contrôler.<br>
'''ὑποτίθημι (verbe)''' : supposer.<br>
'''ὑπότριμμα, -ίμματος (nom commun) (n)''' : Sauce aux herbes, sauce verte et piquante.<br>
'''ὑπουργεῖον, -ίου (nom commun) (n)''' : ministère.<br>
'''ὑπουργικός, -ή, -όν (adjectif)''' : ministériel.<br>
'''ὑπουργός, -οῦ (nom commun) (m)''' : ministre.<br>
'''ὑποχώρησις, -ήσεως (nom commun) (f)''' : régression.<br>
'''ὑποχωρητικός, -ή, -όν (adjectif)''' : régressif.<br>
'''ὑποψήφιος, -ίου (nom commun) (m)''' : candidat.<br>
'''ὑπό (adverbe ; préposition)''' : en dessous.<br>
'''ὑπο- (préfixe)''' : relatif au dessous.<br>
'''ὕραξ, -κος (nom commun) (m)''' : souris.<br>
'''ὕσσωπος, -ώπου (nom commun) (f)''' : hysope.<br>
'''ὕστατος, -η, -ον (adjectif)''' : dernier.<br>
'''ὑστερέω (verbe)''' : .<br>
'''ὑστέρησις, -ήσεως (nom commun) (f)''' : hystérèse.<br>
'''ὑστερικός, -ή, -όν (adjectif)''' : utérin ; hystérique.<br>
'''ὑστερο- (préfixe)''' : suivant ; tardif.<br>
'''ὕστερος, -α, -ον (adjectif)''' : postérieur.<br>
'''ὕστερος, -έρου (nom commun) (m)''' : matrice ; utérus.<br>
'''ὑστέρως (adverbe)''' : .<br>
'''ὕστριξ, -χός (nom commun) (m/f)''' : porc-épic.<br>
'''ὗς, -ός (nom commun) (m/f)''' : Porc, sanglier. Truie, laie.<br>
'''ὑφαίνω (verbe)''' : tisser.<br>
'''ὕφαλος, -άλου (nom commun) (m)''' : récif.<br>
'''ὑφή, -ῆς (nom commun) (f)''' : toile d’araignée.<br>
'''ὕφος, -ους (nom commun) (n)''' : tissu.<br>
'''ὑψηλός, -ή, -όν (adjectif)''' : .<br>
'''ὕψι (adverbe)''' : en haut.<br>
'''ὖ ψιλόν (nom commun) (n)''' : upsilon.<br>
'''ὑψίτερος, -έρη, -ερον (adverbe)''' : Comparatif de ''ὕψι''.<br>
'''ὕψιστος, -ίστη, -ιστον (adverbe)''' : Superlatif de ''ὕψι''.<br>
'''ὕψος, -ους (nom commun) (n)''' : hauteur.<br>
'''ὕψωμα, -ώματος (nom commun) (n)''' : élévation.<br>
'''ὕψωσις, -ώσεως (nom commun) (f)''' : élévation.<br>
'''ὑψῶ (verbe)''' : élever.<br>
'''ὕω (verbe)''' : pleuvoir.<br>
'''Ὕϐρις, -εως (nom propre) (f)''' : Hybris.<br>
'''Ὑδροχόος, -ου (nom propre) (m)''' : Verseau.<br>
'''Ὑγίεια, -ίας (nom propre) (f)''' : [[wikt:Hygie|Hygie]].<br>
'''Ὕηττος, -ήττου (nom propre) (m)''' : Hyettos.<br>
'''Ὑκσώς, -οῦς (nom commun) (m)''' : Hyksôs.<br>
'''Ὕλλος, -ου (nom propre) (m)''' : Hyllos.<br>
'''Ὑμέναιος, -ίου (nom propre) (m)''' : [[wikt:Hyménée|Hyménée]].<br>
'''Ὑμήν, -ένος (nom propre) (m)''' : Variante de ''Ὑμέναιος''.<br>
'''Ὑπερίων, -ος (nom propre) (m)''' : Hypérion.<br>
'''Ὕπνος, -ου (nom propre) (m)''' : [[wikt:Hypnos|Hypnos]].<br>
'''Ὑστάσπης, -ου (nom propre) (m)''' : Hystaspès.<br>
'''Ὕψιστος, -ίστου (nom propre) (m)''' : Très-Haut.<br>
==Φ==
'''φαγός, -οῦ (nom commun) (m)''' : Forme dorienne de ''φηγός''.<br>
'''φαιδρός, -ίδρα, -ιδρόν (adjectif)''' : brillant ; rayonnant, enjoué ; gai, jovial.<br>
'''φαίνεσθαι (verbe)''' : se montrer.<br>
'''φαινόμενον, -ένου (nom commun) (n)''' : phénomène.<br>
'''φαίνω (verbe)''' : faire briller.<br>
'''φακός, -οῦ (nom commun) (m)''' : Lentille. (Anatomie) Cristallin. Tache de rousseur.<br>
'''φακόχοιρος, -ίρου (nom commun) (m)''' : phacochère.<br>
'''φάλαγξ, -γος (nom commun) (f)''' : phalange.<br>
'''φάλλαινα, -ίνης (nom commun) (f)''' : baleine.<br>
'''φαλλός, -οῦ (nom commun) (m)''' : phallus.<br>
'''φάμη, -ης (nom commun) (f)''' : Forme dorienne de ''φήμη''.<br>
'''φαμί (verbe)''' : Forme dorienne de ''φημί''.<br>
'''φάναξ, -κος (nom commun) (m)''' : .<br>
'''φανερός, -ά, -όν (adjectif)''' : Apparent. En vue.<br>
'''φανερῶς (adverbe)''' : .<br>
'''φανερώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''φανερός''.<br>
'''φανερώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''φανερός''.<br>
'''φανερῶ (verbe)''' : .<br>
'''φανός, -οῦ (nom commun) (m)''' : .<br>
'''φάνταγμα, -άγματος (nom commun) (n)''' : Forme ionienne de ''φάντασμα''.<br>
'''φαντάζω (verbe)''' : Dénoncer ; Se montrer, apparaître.<br>
'''φαντασία, -ας (nom commun) (f)''' : imagination.<br>
'''φάντασμα, -άσματος (nom commun) (n)''' : Apparition, vision, songe ; fantôme, spectre.<br>
'''φανταστής, -οῦ (nom commun) (m)''' : vantard.<br>
'''φανταστικός, -ή, -όν (adjectif)''' : imaginaire.<br>
'''φάραγξ, -γος (nom commun) (m)''' : canyon.<br>
'''φαραώ (nom commun) (m)''' : pharaon.<br>
'''φαρέτρα, -ας (nom commun) (f)''' : carquois.<br>
'''φαρέτρη, -ης (nom commun) (f)''' : Forme dorienne de ''φαρέτρα''.<br>
'''φαρετρεών, -ῶνος (nom commun) (m)''' : .<br>
'''φαρισαϊκός, -ή, -όν (adjectif) (f)''' : pharisien.<br>
'''φαρισαϊκότατα, -, - (adverbe)''' : Superlatif de ''φαρισαϊκῶς''.<br>
'''φαρισαϊκότερον, -, - (adverbe)''' : Comparatif de ''φαρισαϊκῶς''.<br>
'''φαρισαϊκῶς (adverbe)''' : pharisiennement.<br>
'''φαρισαϊκώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''φαρισαϊκός''.<br>
'''φαρισαϊκώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''φαρισαϊκός''.<br>
'''φαρισαῖος, -ίου (nom commun) (m)''' : pharisien.<br>
'''φαρμακεία, -ας (nom commun) (f)''' : ensemble des médicaments.<br>
'''φαρμακεύς, -έως (nom commun) (m)''' : Apothicaire, empoisonneur. Sorcier.<br>
'''φαρμακευτικός, -ή, -όν (adjectif) (f)''' : de remède.<br>
'''φαρμακευτικότατα, -, - (adverbe)''' : Superlatif de ''φαρμακευτικῶς''.<br>
'''φαρμακευτικότερον, -, - (adverbe)''' : Comparatif de ''φαρμακευτικῶς''.<br>
'''φαρμακευτικῶς (adverbe)''' : .<br>
'''φαρμακευτικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''φαρμακευτικός''.<br>
'''φαρμακευτικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''φαρμακευτικός''.<br>
'''φαρμακίς, -δος (nom commun) (f)''' : Apothicaire, empoisonneuse. Sorcière.<br>
'''φάρμακον, -άκου (nom commun) (n)''' : Médicament, remède ; poison, préparation magique. Teinture, fard.<br>
'''φαρμακοποσία, -ας''' : action de boire une potion.<br>
'''φαρμακοπώλης, -ου (nom commun) (m)''' : pharmacien.<br>
'''φαρμακός, -οῦ (nom commun) (m)''' : victime expiatoire.<br>
'''φαρμακοτρίϐης, -ου (nom commun) (m)''' : laborantin.<br>
'''φαρμακώδης, -ης, -ες (adjectif)''' : médicinal ; vénéneux.<br>
'''φαρμακῶ (verbe)''' : empoisonner.<br>
'''φάρυγξ, -γος (nom commun) (m)''' : gosier.<br>
'''φάσηλος, -ήλου (nom commun) (m)''' : haricot sec.<br>
'''φασιανικός, -ή, -όν (adjectif)''' : phasianien.<br>
'''φασιανός, -οῦ (nom commun) (m)''' : faisan.<br>
'''φάκελος, -έλου (nom commun) (m)''' : fagot, faisceau.<br>
'''φάσκος, -ου (nom commun) (m)''' : .<br>
'''φασκώλιον, -ίου (nom commun) (n)''' : valisette.<br>
'''φάσκωλος, -ώλου (nom commun) (m)''' : valise.<br>
'''φάσμα, -τος (nom commun) (n)''' : spectre.<br>
'''φασματικός, -ή, -όν (adjectif)''' : spectral.<br>
'''φασματικώτατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''φασματικός''.<br>
'''φασματικώτερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''φασματικός''.<br>
'''φασματικῶς (adverbe)''' : spectralement.<br>
'''φασματικώτατα, -, - (adverbe)''' : Superlatif de ''φασματικῶς''.<br>
'''φασματικώτερον, -, - (adverbe)''' : Comparatif de ''φασματικῶς''.<br>
'''φάτις, -εως (nom commun) (f)''' : rumeur, parole.<br>
'''φάττα, -ης (nom commun) (f)''' : palombe ; ramier.<br>
'''φαῦλος, -ύλη, -ῦλον (adjectif)''' : vicieux.<br>
'''φέγγος, -ους (nom commun) (n)''' : .<br>
'''φέρετρον, -έτρου (nom commun) (n)''' : cercueil.<br>
'''φέρω (verbe)''' : porter.<br>
'''φεύγω (verbe)''' : fuir (prendre la fuite).<br>
'''φεῦξις, -ύξεως (nom commun) (f)''' : échappée, évasion ; fuite.<br>
'''φεῦ (interjection)''' : Hélas ; ah.<br>
'''φή (conjonction)''' : Forme homérique de ''ὡς''.<br>
'''φηγός, -οῦ (nom commun) (f)''' : chêne.<br>
'''φηλητής, -οῦ (nom commun) (m)''' : trompeur.<br>
'''φῆλος, -ος, -ον (adjectif)''' : trompeur.<br>
'''φήμη, -ης (nom commun) (f)''' : Voix prophétique, oracle. Rumeur, réputation.<br>
'''φημί (verbe)''' : dire.<br>
'''φήρ, -ός (nom commun) (m)''' : Forme éolienne de ''θήρ''.<br>
'''φθέγγομαι (verbe)''' : proférer.<br>
'''φθειρρός, -οῦ (nom commun) (m)''' : pou.<br>
'''φθείρω (verbe)''' : user.<br>
'''φθινόπωρον, -ώρου (nom commun) (n)''' : automne.<br>
'''φθίσις, -εως (nom commun) (f)''' : Déclin, ruine. Atrophie, consomption. Contraction, réduction de la pupille.<br>
'''φθίω (verbe)''' : périr, disparaitre.<br>
'''φθόγγος, -ου (nom commun) (m)''' : son.<br>
'''φθονερός, -ή, -όν (adjectif) (f)''' : envieux.<br>
'''φθόνησις, -ήσεως (nom commun) (f)''' : dénégation.<br>
'''φθόνος, -ου (nom commun) (m)''' : envie.<br>
'''φθονῶ (verbe)''' : envier.<br>
'''φθορά, -ᾶς (nom commun) (f)''' : Perdition, perte, ruine, destruction. Action de corrompre, corruption, séduction. (Peinture) Dégradation de couleur, affaiblissement de teinte.<br>
'''φῖ (nom commun) (n)''' : phi.<br>
'''φίλαμα, -άματος (nom commun) (n)''' : Forme dorienne de ''φίλημα''.<br>
'''φιλαδελφία, -ας (nom commun) (f)''' philadelphie.<br>
'''φιλανδρία, -ας (nom commun) (f)''' philandrie.<br>
'''φιλανθρωπία, -ας (nom commun) (f)''' philanthropie.<br>
'''φιλάνθρωπος, -ος, -ον (adjectif)''' : Humain, bon ; bienveillant, affable. Qui aime les hommes (en parlant des dieux). Qui plaît aux hommes, agréable.<br>
'''φιλαργυρία, -ας (nom commun) (f)''' : avarice.<br>
'''φιλάργυρος, -ος, -ον (adjectif)''' : avare.<br>
'''φίλειμι (verbe)''' : Forme béotienne de ''φιλῶ''.<br>
'''φίλημμι (verbe)''' : Forme éolienne de ''φιλῶ''.<br>
'''φιλία, -ας (nom commun) (f)''' : amitié, amour absolu, plaisir de la compagnie.<br>
'''φιλίη, -ης (nom commun) (f)''' : Forme ionienne de ''φιλία''.<br>
'''φίλημα, -ήματος (nom commun) (n)''' : baiser.<br>
'''φίλημμι (verbe)''' : Forme éolienne de ''φιλῶ''.<br>
'''φιλόγελως, -ωτος (nom commun) (m)''' : Amateur d'histoires drôles.<br>
'''φιλόπαις, -δός (nom commun) (m)''' : philopaide.<br>
'''φίλος, -η, -ον (adjectif)''' : amical.<br>
'''φίλος, -ου (nom commun) (m)''' : ami.<br>
'''φιλο- (préfixe)''' : qui aime.<br>
'''φιλογυνία, -ας (nom commun) (f)''' : philogynie.<br>
'''φιλοπαιδία, -ας (nom commun) (f)''' : philopaidie.<br>
'''φιλόπαις, -αίδος (nom commun) (m)''' : philopaide.<br>
'''φιλοσοφία, -ας (nom commun) (f)''' : philosophie.<br>
'''φιλοσοφικός, -ή, -όν (adjectif)''' : philosophique.<br>
'''φιλοσοφικότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''φιλοσοφικός''.<br>
'''φιλοσοφικότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''φιλοσοφικός''.<br>
'''φιλοσοφικῶς (adverbe)''' : philosophiquement.<br>
'''φιλοσοφικώτατα, -, - (adverbe)''' : Superlatif de ''φιλοσοφικῶς''.<br>
'''φιλοσοφικώτερον, -, - (adverbe)''' : Comparatif de ''φιλοσοφικῶς''.<br>
'''φιλόσοφος, -όφου (nom commun) (m)''' : philosophe.<br>
'''φιλοσοφῶ (verbe)''' : philosopher.<br>
'''φιλότης, -τος (nom commun) (f)''' : Amitié ; relation sexuelle.<br>
'''φιλύρα, -ας (nom commun) (f)''' : tilleul.<br>
'''φιλῶ (verbe)''' : Aimer d'amitié. Éprouver de l'amitié. Traiter en ami, regarder comme un ami. Donner un signe d'amitié. Aimer d'amour. (Par extension) Aimer. Voir volontiers, accueillir avec plaisir, approuver, agréer. Rechercher, poursuivre. Se plaire à.<br>
'''φίλως (adverbe)''' : amicalement.<br>
'''φιμός, -οῦ (nom commun) (m)''' : .<br>
'''φίμωσις, -ώσεως (nom commun) (f)''' : musellement.<br>
'''φίμωτρον, -ου (nom commun) (m)''' : muselière.<br>
'''φιμῶ (verbe)''' : museler.<br>
'''φιτύω (verbe)''' : engendrer.<br>
'''φλέγμα, -τος (nom commun) (n)''' : flegme.<br>
'''φλέγω (verbe)''' : brûler.<br>
'''φλέψ, -ϐός (nom commun) (f)''' : veine (vaisseau sanguin).<br>
'''φλέω (verbe)''' : sourdre.<br>
'''φλίϐω (verbe)''' : étendre, presser.<br>
'''φλόγωσις, -ώσεως (nom commun) (m)''' : inflammation.<br>
'''φλογῶ (verbe)''' : flamber.<br>
'''φλοιός, -οῦ (nom commun) (m)''' : écorce.<br>
'''φλόξ, -γός (nom commun) (m)''' : flamme.<br>
'''φλύαρος, -ος, -ον (adjectif)''' : bavard.<br>
'''φλυαρῶ (verbe)''' : bavarder.<br>
'''φλύκταινα, -ίνας (nom commun) (f)''' : cloque.<br>
'''φλύζω (verbe)''' : Forme de ''φλύω''.<br>
'''φλύω (verbe)''' : couler.<br>
'''φοϐερός, -ά, -όν (adjectif)''' : effrayant.<br>
'''φόϐος, -ου (nom commun) (m)''' : peur.<br>
'''φοῖϐος, -η, -ον (adjectif)''' : .<br>
'''φοῖνιξ, -ίνικος (nom commun) (m)''' : palmier-dattier ; phénix.<br>
'''φονεύς, -έως (nom commun) (m)''' : meurtrier.<br>
'''φονεύω (verbe)''' : assassiner.<br>
'''φονικός, -ή, -όν (adjectif)''' : meurtrier.<br>
'''φόνος, -ου (nom commun) (m)''' : meurtre.<br>
'''φόρημα, -ήματος (nom commun) (n)''' : .<br>
'''φόρμιγξ, -γος (nom commun) (f)''' : (poésie) lyre.<br>
'''φορτίζω (verbe)''' : charger.<br>
'''φόρτισις, -ίσεως (nom commun) (f)''' : charge.<br>
'''φόρτος, -ου (nom commun) (m)''' : .<br>
'''φοῦκτα, -ύκτας (nom commun) (m)''' : poignée de main.<br>
'''φοῦρνος, -ύρνου (nom commun) (m)''' : four.<br>
'''φραγή, -ῆς (nom commun) (f)''' : bloc.<br>
'''φράγμα, -ατος (nom commun) (n)''' : clôture.<br>
'''φραγμός, -οῦ (nom commun) (m)''' : bloc.<br>
'''φράν, -ός (nom commun) (f)''' : Forme dorienne de ''φρήν''.<br>
'''φράσις, -εως (nom commun) (f)''' : Suite de mots.<br>
'''φράσσω (verbe)''' : déclarer.<br>
'''φράτηρ, -ερος (nom commun) (m)''' : Membre d'une phratrie.<br>
'''φρατήρ, -έρος (nom commun) (m)''' : Forme dorienne de ''φράτηρ''.<br>
'''φρατρία, -ας (nom commun) (f)''' : phratrie.<br>
'''φρέαρ, -τος (nom commun) (n)''' : puits.<br>
'''φρενητικός, -ή, -όν (adjectif)''' : délirant.<br>
'''φρενητικῶς (adverbe)''' : -ment.<br>
'''φρενητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''φρενητικός''.<br>
'''φρενητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''φρενητικός''.<br>
'''φρενητικώτατα, -, - (adverbe)''' : Superlatif de ''φρενητικῶς''.<br>
'''φρενητικώτερον, -, - (adverbe)''' : Comparatif de ''φρενητικῶς''.<br>
'''φρενῖτις, -ίτιδος (nom commun) (f)''' : délire.<br>
'''φρήν, -ενός (nom commun) (f)''' : Toute membrane qui enveloppe un organe. (Au pluriel) Viscères, entrailles. (Par suite) (Poésie) Cœur, âme.<br>
'''φρήτηρ, -ερος (nom commun) (m)''' : Forme ionienne de ''φράτηρ''.<br>
'''φρίκη, -ης (nom commun) (f)''' : terreur.<br>
'''φρίξ, -ικός (nom commun) (f)''' : terreur.<br>
'''φριξός, -ή, -όν (adjectif)''' : terrifiant.<br>
'''φρίσσω (verbe)''' : terroriser.<br>
'''φρόνημα, -ήματος (nom commun) (n)''' : Intelligence, pensée. Manière de penser.<br>
'''φρόνησις, -ήσεως (nom commun) (f)''' : Pensée, dessein. Intelligence raisonnable, sagesse. Intelligence (ou sagesse) divine.<br>
'''φρόνιμος, -ος, -ον (adjectif)''' : sensé.<br>
'''φρονῶ (verbe)''' : Penser, avoir la faculté de penser ou de sentir, vivre. Être dans son bon sens. Penser. Être sensé. Avoir dans l’esprit. Songer à, projeter de.<br>
'''φρουρά, -ᾶς (nom commun) (m)''' : garde (corps d’armée).<br>
'''φρουρή, -ῆς (nom commun) (m)''' : Forme ionienne de ''φρουρή''.<br>
'''φρούρημα, -ήματος (nom commun) (n)''' : bannissement.<br>
'''φρούρησις, -ήσεως (nom commun) (f)''' : fuite, bannissement.<br>
'''φρουρητός, -ός, -όν (adjectif)''' : banni.<br>
'''φρούριον, -ίου (nom commun) (n)''' : forteresse.<br>
'''φρουρός, -οῦ (nom commun) (m)''' : garde (surveillant).<br>
'''φρουρῶ (verbe)''' : bannir.<br>
'''φρύγω (verbe)''' : rôtir.<br>
'''φρῦνος, -ύνου (nom commun) (m)''' : crapaud.<br>
'''φυγάς, -δος (nom commun) (m)''' : fugitif.<br>
'''φυγή, -ῆς (nom commun) (f)''' : fuite, bannissement.<br>
'''φύζω (verbe)''' : Forme ionienne de ''φεύγω''.<br>
'''φυίω (verbe)''' : Forme éolienne de ''φύω''.<br>
'''φυλακή, -ῆς (nom commun) (f)''' : garde, surveillance ; vigilance.<br>
'''φυλακτήριον, -ίου (nom commun) (n)''' : amulette.<br>
'''φυλακτήρ, -ῆρος (nom commun) (m)''' : garde.<br>
'''φυλάξις, -εως (nom commun) (f)''' : garde.<br>
'''φύλαξ, -κος (nom commun) (m)''' : observateur, garde ; protecteur.<br>
'''φυλάσσω (verbe)''' : garder.<br>
'''φυλάττω (verbe)''' : Forme attique de ''φυλάσσω''.<br>
'''φυλετικός, -ή, -όν (adjectif)''' : racial.<br>
'''φυλή, -ῆς (nom commun) (f)''' : Tribu, groupe de familles de même races ; (Militaire) Corps de troupes au nombre de 10. (Par extension) Classe, genre ; espèce.<br>
'''φύλλον, -ου (nom commun) (n)''' : feuille.<br>
'''φυλλόω (verbe)''' : .<br>
'''φῦλον, -ύλου (nom commun) (n)''' : groupe, tribu ; nation.<br>
'''φῦμα, -ύματος (nom commun) (n)''' : .<br>
'''φυμάτιον, -ίου (nom commun) (n)''' : .<br>
'''φύραμα, -τος (nom commun) (n)''' : .<br>
'''φυράω (verbe)''' : .<br>
'''φύρδην (adverbe)''' : .<br>
'''φυρατέον, -ου (nom commun) (n)''' : .<br>
'''φυρατής, -οῦ (nom commun) (m)''' : .<br>
'''φύρμα, -τος (nom commun) (n)''' : .<br>
'''φυρμός, -οῦ (nom commun) (m)''' : .<br>
'''φύρσιμος, -ίµου (nom commun) (m)''' : .<br>
'''φύρσις, -εως (nom commun) (f)''' : .<br>
'''φύρω (verbe)''' : .<br>
'''φυσσαλίς, -δος (nom commun) (m)''' : bulle.<br>
'''φῦσα, -ύσας (nom commun) (f)''' : .<br>
'''φυσάω (verbe)''' : .<br>
'''φυσητήρ, -ῆρος (nom commun) (m)''' : soupirail.<br>
'''φῦσιγξ, -ύσιγγος (nom commun) (m)''' : cartouche.<br>
'''φύσις, -εως (nom commun) (f)''' : nature.<br>
'''φυσικός, -ή, -όν, (adjectif)''' : naturel.<br>
'''φυσικῶς (adverbe)''' : naturellement.<br>
'''φυσικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''φυσικός''.<br>
'''φυσικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''φυσικός''.<br>
'''φυσικώτατα, -, - (adverbe)''' : Superlatif de ''φυσικῶς''.<br>
'''φυσικώτερον, -, - (adverbe)''' : Comparatif de ''φυσικῶς''.<br>
'''φύσκα, -ας (nom commun) (f)''' : Forme dorienne de ''φύσκη''.<br>
'''φύσκη, -ης (nom commun) (f)''' : .<br>
'''φύτευμα, -ύματος (nom commun) (n)''' : raiponce.<br>
'''φυτεύω (verbe)''' : planter.<br>
'''φυτόν, -οῦ (nom commun) (n)''' : végétal, plante.<br>
'''φυτο- (préfixe)''' : relatif aux plantes.<br>
'''φῦ (interjection)''' : pouah.<br>
'''φύω (verbe)''' : croître.<br>
'''φώκη, -ης (nom commun) (f)''' : phoque.<br>
'''φώκιος, -ος, -ον (adjectif)''' : phocidien.<br>
'''φωλεός, -οῦ (nom commun) (m)''' : tanière, terrier.<br>
'''φωνήεις, -σσα, -ῆεν (adjectif)''' : vocalique.<br>
'''φωνῆεν, -ήεντος (nom commun) (n)''' : voyelle.<br>
'''φωνή, -ῆς (nom commun) (f)''' : voix.<br>
'''φώνημα, -ήματος (nom commun) (n)''' : phonème.<br>
'''φωνητικός, -ή, -όν (adjectif)''' : phonétique.<br>
'''φωνητικῶς (adverbe)''' : phonétiquement.<br>
'''φωνητικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''φωνητικός''.<br>
'''φωνητικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''φωνητικός''.<br>
'''φωνῶ (verbe)''' : Faire entendre un son de voix. Parler haut, dire d'une voix forte, élever la voix Ordonner, commander, prescrire. Parler de. Chanter.<br>
'''φώς, -τός (nom commun) (m)''' : homme.<br>
'''φῶς, -τός (nom commun) (n)''' : lumière, éclair.<br>
'''φωσφορίζων, -ουσα, -ον (adjectif)''' : fluorescent.<br>
'''φωσφορίζω (verbe)''' : fluorescer.<br>
'''φωσφόρος, -ος, -ον (adjectif)''' : qui apporte la lumière.<br>
'''Φαέθουσα, -ας (nom propre) (f)''' : Phaéthuse.<br>
'''Φαέθων, -οντος (nom propre) (m)''' : Phaéton.<br>
'''Φαίδρα, -ας (nom propre) (f)''' : Phèdre.<br>
'''Φαῖδρος, -ίδρου (nom propre) (f)''' : Phèdre.<br>
'''Φαιδρία, -ας (nom propre) (f)''' : Phédrie.<br>
'''Φαμενώθ (nom propre) (m)''' : Phaminoth.<br>
'''Φαντασός, -οῦ (nom propre) (m)''' : Phantasos.<br>
'''Φάρος, -ου (nom propre) (f)''' : Pharos.<br>
'''Φαρμουθί (nom propre) (m)''' : Pharmouti.<br>
'''Φασιανός, -οῦ (nom commun) (m)''' : Phasien.<br>
'''Φᾶσις, -άσιος (nom propre) (m)''' : Phase.<br>
'''Φαῶφι (nom propre) (m)''' : Phaophi.<br>
'''Φειδίας, -ου (nom propre) (m)''' : Phidias.<br>
'''Φερεκύδης, -ου (nom propre) (m)''' : Phérécyde.<br>
'''Φερενίκη, -ης (nom propre) (f)''' : Véronique.<br>
'''Φηγεύς, -έως (nom propre) (m)''' : Phégée.<br>
'''Φθόνος, -ου (nom propre) (m)''' : [[wikt:Phtonos|Phtonos]].<br>
'''Φιλάμμων, -ονος (nom propre) (m)''' : Philammon. (Demi-frère d'Autolycos.)<br>
'''Φιλέας, -ου (nom propre) (m)''' : Philéas.<br>
'''Φίλιππος, -ίππου (nom propre) (m)''' : Philippe.<br>
'''Φιλόγελως, -τος (nom propre) (m)''' : Philogélos. (Recueil d’histoires drôles probablement rédigé au V{{e}} siècle apr. J.-C., attribué à Hiéroclès et Philagrios)<br>
'''Φιλοκτήτης, -ου (nom propre) (m)''' : Philoctète.<br>
'''Φιλομήλη, -ης (nom propre) (f)''' : Philomèle.<br>
'''Φιλότης, -τος (nom propre) (f)''' : [[wikt:Philotès|Philotès]].<br>
'''Φίλων, -ος (nom propre) (m)''' : Philo.<br>
'''Φιλώτας, -ου (nom propre) (m)''' : Philinte.<br>
'''Φινεές, -οῦ (nom propre) (m)''' : Phinées.<br>
'''Φινεύς, -έως (nom propre) (m)''' : Phinée.<br>
'''Φίξ, -γγός (nom propre) (f)''' : Forme béotienne de ''Σφίγξ''.<br>
'''Φλεγέθων, -οντος (nom propre) (m)''' : Phlégéthon.<br>
'''Φλεγύας, -ντος (nom propre) (m)''' : Phlégias. (père de Coronis)<br>
'''Φλέγων, -ος (nom propre) (m)''' : Phlégon.<br>
'''Φοϐητώρ, -όρος (nom propre) (m)''' : Phobétor.<br>
'''Φοίϐη, -ης (nom propre) (f)''' : [[wikt:Phœbé|Phœbé]].<br>
'''Φοιϐίδας, -ου (nom propre) (m)''' : [[wikt:Phébidas|Phébidas]].<br>
'''Φοῖϐος, -ίϐου (nom propre) (m)''' : [[wikt:Phébus|Phébus]].<br>
'''Φοῖνιξ, -ίνικος (nom commun) (m/f)''' : Phénicien. Carthaginois. (les descendants de Phénicie.)<br>
'''Φόϐος, -ου (nom propre) (m)''' : Phobos.<br>
'''Φραόρτης, -ου (nom propre) (m)''' : .<br>
'''Φρυγία, -ας (nom propre) (f)''' : Phrygie.<br>
'''Φρύξ, -γός (nom commun) (m)''' : Phrygien.<br>
'''Φυλεύς, -έως (nom propre) (m)''' : Phylée (fils d’Augias).<br>
'''Φυλλίς, -δος (nom propre) (f)''' : Phyllis.<br>
'''Φυσίγναθος, -άθου (nom propre) (f)''' : Physignathe.<br>
'''Φωκεύς, -έως (nom commun) (m)''' : Phocidien.<br>
'''Φώκαια, -ίας (nom propre) (f)''' : Phocée. (Ancienne cité grecque d’Asie Mineure.)<br>
'''Φωκαιεύς, -έως (nom commun) (m)''' : Phocéen.<br>
'''Φωκαιίς, -δος (nom commun) (f)''' : Phocéenne.<br>
'''Φωκίς, -δος (nom propre) (f)''' : Phocide.<br>
'''Φῶκος, -ώκου (nom propre) (m)''' : Phocus.<br>
'''Φορμίων, -ος (nom propre) (m)''' : Phormion.<br>
'''Φωτεινή, -ῆς (nom propre) (f)''' : Photine.<br>
==Χ==
'''χαίνω (verbe)''' : .<br>
'''χαῖρε (interjection)''' (Devient ''χαίρετε'' au pluriel.) : bonjour ; salut.<br>
'''χαίρω (verbe)''' : se réjouir ; être joyeux. Se réjouir d'ordinaire, se plaire d'ordinaire à ou dans. Avoir sujet de se réjouir.<br>
'''χάλαζα, -άζης (nom commun) (f)''' : grêle.<br>
'''χαλαρός, -ά, -όν (adjectif)''' : détendu.<br>
'''χαλαρότης, -τος (nom commun) (f)''' : détente.<br>
'''χαλάρωσις, -ώσεως (nom commun) (f)''' : détente.<br>
'''χαλαρῶς (adverbe)''' : .<br>
'''χαλαρῶ (verbe)''' : détendre.<br>
'''χαλάω (verbe)''' : Lâcher, relâcher. Laisser tomber, baisser, rabaisser. Laisser aller. (Au passif) Être adouci. Être indulgent. Céder le passage. Affaiblir.<br>
'''χάλασις, -άσεως (nom commun) (f)''' : Relâchement, relaxation.<br>
'''χαλεπός, -ή, -όν (adjectif)''' : difficile.<br>
'''χαλεπῶς (adverbe)''' : difficilement.<br>
'''χαλεπώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''χαλεπός''.<br>
'''χαλεπώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''χαλεπός''.<br>
'''χάλιξ, -κος (nom commun) (m/f)''' : galet ; gravier.<br>
'''χαλκεύς, -έως (nom commun) (m)''' : forgeron.<br>
'''χαλκός, -οῦ (nom commun) (m)''' : cuivre ; bronze, fer.<br>
'''χαλυϐήϊος, -ΐα, -ήϊον (adjectif)''' : d’acier.<br>
'''χάλυψ, -ϐος (nom commun) (m)''' : acier.<br>
'''χαμᾶζε (adverbe)''' : à terre (avec mouvement).<br>
'''χαμαί (adverbe)''' : à terre (sans mouvement).<br>
'''χαμαικέρασος, -άσου (nom commun) (f)''' : fraise.<br>
'''χαμαιλέων, -οντος (nom commun) (m)''' : caméléon.<br>
'''χαμαίμηλον, -ήλου (nom commun) (f)''' : camomille.<br>
'''χαμαιτυπεῖον, -ίου (nom commun) (n)''' : lupanar.<br>
'''χαμαιτύπη, -ης (nom commun) (f)''' : prostituée.<br>
'''χάν, -ός (nom commun) (m/f)''' : Forme dorienne de ''χήν''.<br>
'''χάος, -ους (nom commun) (n)''' : chaos.<br>
'''χαρά, -ᾶς (nom commun) (f)''' : joie ; plaisir.<br>
'''χαρακτηρίζω (verbe)''' : inscrire, marquer.<br>
'''χαρακτήρ, -ῆρος (nom commun) (m)''' : Fer pour marquer le bétail, marque faite au fer rouge. Marque, signe, empreinte, stigmate. Cachet, caractère, genre de style, style.<br>
'''χαράσσω (verbe)''' : Aiguiser. Gratter, couper.<br>
'''χάραξ, -κος (nom commun) (m)''' : pieu.<br>
'''χαριέντως (adverbe)''' : gracieusement.<br>
'''χαρίεις, -εσσα, -εν (adjectif)''' : gracieux.<br>
'''χαρίϝεις, -εσσα, -εν (adjectif)''' : Forme homérique de ''χαρίεις''.<br>
'''χαριέστατος, -άτη, -έστατον (adjectif)''' : Superlatif de ''χαρίεις''.<br>
'''χαριέστερος, -έρα, -ερον (adjectif)''' : Comparatif de ''χαρίεις''.<br>
'''χαρίϝεττα, -, - (adjectif)''' : Forme béotienne de ''χαρίεις''.<br>
'''χαρίζομαι (verbe)''' : .<br>
'''χάρις, -τος (nom commun) (f)''' : Ce qui brille ; ce qui réjouit. Grâce.<br>
'''χάρισμα, -ίσματος (nom commun) (n)''' : grâce accordée par Dieu.<br>
'''χαριτῶ (verbe)''' : .<br>
'''χάρμα, -τος (nom commun) (n)''' : source de joie, délice.<br>
'''χάσμα, -τος (nom commun) (n)''' : béance, ouverture ; creux, gouffre. Abysse.<br>
'''χάσκω (verbe)''' : bayer, béer.<br>
'''χαῦνος, -ύνη, -ῦνον (adjectif)''' : .<br>
'''χαυνότης, -τος (suffixe) (f)''' : .<br>
'''χαυνῶ (verbe)''' : .<br>
'''χέζω (verbe)''' : chier.<br>
'''χεῖμα, -ίματος (nom commun) (n)''' : hiver.<br>
'''χείμαρρος, -άρρου (nom commun) (m)''' : torrent.<br>
'''χειμών, -ῶνος (nom commun) (m)''' : (sens propre) Mauvais temps. (sens figuré) Trouble.<br>
'''χειραφέτησις, -ήσεως (nom commun) (f)''' : émancipation.<br>
'''χείριστος, -ίστη, -ίριστον (adjectif)''' : Superlatif de ''κακός''.<br>
'''χείρ, -ός (nom commun) (f)''' : main.<br>
'''χειρόγραφον, -άφου (nom commun) (n)''' : manuscrit.<br>
'''χειρόγραφος, -η, -ον (adjectif)''' : manuscrit.<br>
'''χείρων, -ων, -ῖρον (adjectif)''' : Comparatif de ''κακός''.<br>
'''χελιδών, -ονός (nom commun) (f)''' : hirondelle.<br>
'''χελώνη, -ης (nom commun) (f)''' : tortue.<br>
'''χερνίϐιον, -ίου (nom commun) (n)''' : pot de chambre.<br>
'''χερσόνησος, -ήσου (nom commun) (f)''' : péninsule.<br>
'''χέρσος, -ος, -ον (adjectif)''' : sec ; stérile.<br>
'''χέω (verbe)''' : jouir. (Profiter d'une chose que l'on possède.)<br>
'''χηλή, -ῆς (nom commun) (f)''' : pince ; serre de certains animaux.<br>
'''χηλός, -οῦ (nom commun) (m)''' : coffre.<br>
'''χήν, -ός (nom commun) (m/f)''' : jars ; oie.<br>
'''χήρα, -ας (nom commun) (f)''' : veuve.<br>
'''χῆρος, -ήρου (nom commun) (m)''' : veuf.<br>
'''χθές (adverbe)''' : hier.<br>
'''χθόνιος, -α, -ον (adjectif)''' : Relatif aux divinités infernales.<br>
'''χθών, -ονός (nom commun) (f)''' : Sol, terre. Terre, pays, contrée. Ensemble du sol terrestre, terre entière. Terre comme séjour des vivants et des morts.<br>
'''χῖ (nom commun) (n)''' : chi.<br>
'''χίλιοι (adjectif numéral)''' : mille.<br>
'''χιόνεος, -έα, -εον (verbe)''' : enneigé.<br>
'''χιονίζω (verbe)''' : enneiger.<br>
'''χιονόχρως (adjectif)''' : blanc comme neige.<br>
'''χιτών, -ῶνος (nom commun) (m)''' : chiton.<br>
'''χιών, -όνος (nom commun) (f)''' : neige.<br>
'''χλαμύς, -δος (nom commun) (n)''' : chlamyde.<br>
'''χλευάζω (verbe)''' : se moquer.<br>
'''χλευασία, -ας (nom commun) (f)''' : moquerie.<br>
'''χλευασμός, -οῦ (nom commun) (m)''' : Moquerie ; blague.<br>
'''χλιαρός, -ή, -όν (adjectif)''' : tiède.<br>
'''χλωμός -ή -όν (adjectif)''' : pâle.<br>
'''χλωρός, -ά, -όν (adjectif)''' : Vert ; jaune verdâtre.<br>
'''χοῖρος, -ίρου (nom commun) (m)''' : Petit cochon. (Par extension) Cochon engraissé, porc.<br>
'''χόνδρος, -ου (nom commun) (m)''' : Petit corps dur et rond, grain. Grain de froment et d’épeautre. Froment, épeautre. Cartilage.<br>
'''χονδρός, -ά, -όν (adjectif)''' : gros.<br>
'''χονδρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''χονδρός''.<br>
'''χονδρότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''χονδρός''.<br>
'''χονδρύνω (verbe)''' : grossir.<br>
'''χονδρῶς (adverbe)''' : .<br>
'''χοραύλης, -ου (nom commun) (m)''' : choraule.<br>
'''χοραυλικός, -ή, -όν (adjectif)''' : choraulique.<br>
'''χορδή, -ῆς (nom commun) (f)''' : ficelle.<br>
'''χορεία, -ας (nom commun) (f)''' : danse.<br>
'''χορεῖος, -ίου (nom commun) (m)''' : chorée.<br>
'''χορευτής, -οῦ (nom commun) (m)''' : danseur.<br>
'''χορεύω (verbe)''' : danser.<br>
'''χορήγιον, -ίου (nom commun) (m)''' : chorégie.<br>
'''χορηγός, -οῦ (nom commun) (m)''' : chorège.<br>
'''χορικός, -ή, -όν (adjectif)''' : chorique.<br>
'''χοροκιθαριστής, -οῦ (nom commun) (m)''' : chorocithariste.<br>
'''χορός, -οῦ (nom commun) (m)''' : danse en ronde.<br>
'''χόρτος, -ου (nom commun) (m)''' : Enclos. Pré clôturé ; pâturage. Nourriture.<br>
'''χορῳδία, -ας (nom commun) (f)''' : .<br>
'''χράω (verbe)''' : Frapper, attaquer. Infliger, asséner. Désirer ardemment. Proclamer. (Au passif) Être déclaré par un oracle. (Voix moyenne) Consulter un oracle, être averti par un oracle. Fournir, prêter. Utiliser. Avoir besoin de.<br>
'''χρεία, -ας (nom commun) (f)''' : Usage, emploi. Manière dont on fait usage. Profit qu'on retire d'un usage. Besoin, nécessité.<br>
'''χρείη, -ης (nom commun) (f)''' : Forme ionienne de ''χρεία''.<br>
'''χρεώ (verbe)''' : avoir besoin.<br>
'''χρῄζω (verbe)''' : nécessiter, désirer ; vouloir.<br>
'''χρή (verbe)''' : il faut.<br>
'''χρῆμα, -ήματος (nom commun) (n)''' : Chose dont on se sert, ou dont on a besoin. (Au singulier) Ce dont on se sert, chose, affaire. Évènement, occurrence. (Au pluriel) Bien, avoir.<br>
'''χρῆσις, -ήσεως (nom commun) (f)''' : .<br>
'''χρησμός, -οῦ (nom commun) (f)''' : oracle.<br>
'''χρηστομάθεια, -ίας (nom commun) (f)''' : savoir utile.<br>
'''χρηστός, -ή -όν (adjectif)''' : Dont on peut se servir. Qui rend service.<br>
'''χρηστότης, -τος (nom commun) (f)''' : Bonne qualité, bonté. (En particulier) Bonté de cœur.<br>
'''χρίζω (verbe)''' : oindre.<br>
'''χρῖσμα, -ίσματος (nom commun) (n)''' : onction.<br>
'''χρονικός, -ή -όν (adjectif)''' : temporel.<br>
'''χρονικῶς (adverbe)''' : temporellement.<br>
'''χρονικώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''χρονικός''.<br>
'''χρονικώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''χρονικός''.<br>
'''χρόνιος, -ία -όνιον (adjectif)''' : tardif.<br>
'''χρονίως (adverbe)''' : tardivement.<br>
'''χρονιώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''χρονίως''.<br>
'''χρονιώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''χρονίως''.<br>
'''χρόνος, -ου (nom commun) (m)''' : temps (durée).<br>
'''χρυσαίετος, -ου (nom commun) (m)''' : aigle royal.<br>
'''χρυσάνθεμον, -έμου (nom commun) (n)''' : chrysanthème.<br>
'''χρυσόγονον, -όνου (nom commun) (n)''' : navet noir.<br>
'''χρυσομηλιά, -ᾶς (nom commun) (f)''' : orange.<br>
'''χρυσός, -οῦ (nom commun) (m)''' : or.<br>
'''χρῶμα, -ώματος (nom commun) (n)''' : couleur.<br>
'''χυδαῖος, -ίη ,-ῖον (adjectif)''' : s’affaissant ; commun. Vulgaire.<br>
'''χυδαιολογία, -ας (nom commun) (f)''' : .<br>
'''χυλός, οῦ (nom commun) (m)''' : gruau.<br>
'''χυλοπίτα, -ας (nom commun) (f)''' : .<br>
'''χυλώδης, -ης, -ες (adjectif)''' : .<br>
'''χύλωμα, -τος (nom commun) (n)''' : .<br>
'''χυλώνω (verbe)''' : .<br>
'''χύμα, -τος (nom commun) (n)''' : suc.<br>
'''χυμεία, -ας (nom commun) (f)''' : .<br>
'''χυµός, -οῦ (nom commun) (m)''' : jus.<br>
'''χωλαίνω (verbe)''' : boiter.<br>
'''χωλίαμϐος, -άμϐου (nom commun) (m)''' : choliambe.<br>
'''χωλός, -ή, -όν (adjectif)''' : boiteux. (En parlant de l'intelligence) Dont l'esprit cloche, infirme d'esprit. Mal équilibré, instable, chancelant.(Rhétorique) Boiteux. (Métrique) Qui n'est pas sur ses pieds, boiteux.<br>
'''χωλότης, -τος (nom commun) (f)''' : .<br>
'''χωλῶς (adverbe)''' : en boitant.<br>
'''χωλότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''χωλός''.<br>
'''χωλότερος, -έρα, -ότερον (adjectif)''' : Comparatif de ''χωλός''.<br>
'''χῶμα, -ώματος (nom commun) (n)''' : (Militaire) Terrassement. (Par extension) Toute jetée, même en pierres, môle.<br>
'''κώμη, -ης (nom commun) (f)''' : village.<br>
'''χώρα, -ας (nom commun) (f)''' : Lieu. Place, endroit. (Militaire) Poste, position. (Au sens abstrait) Moment. Pays, région. Campagne. Champ, ferme.<br>
'''χώρη, -ης (nom commun) (f)''' : Forme ionienne de ''χώρα''.<br>
'''χωρίζω (verbe)''' : séparer.<br>
'''χωρίον, -ου (nom commun) (n)''' : district.<br>
'''χωρισμός, -οῦ (nom commun) (m)''' : séparation.<br>
'''χωρίς (adverbe)''' : Séparément ; à part. (Avec le génitif) sans. (Par suite) différemment.<br>
'''Χαιρέας, -ου (nom propre) (m)''' : Chéréas.<br>
'''Χαιρεφῶν, -ῶντος (nom propre) (m)''' : Chariphon.<br>
'''Χαιρώνεια, -ίας (nom propre) (f)''' : Chéronée (cité de Béotie).<br>
'''Χαιρωνεύς, -έως (nom propre) (m)''' : Chéronéen.<br>
'''Χαλδαία, -ας (nom propre) (f)''' : Chaldée.<br>
'''Χαλδαῖα, -ίας (nom commun) (f)''' : Chaldéenne.<br>
'''Χαλδαϊκός, -ή, -όν (adjectif)''' : chaldéen.<br>
'''Χαλδαῖος, -ίου (nom commun) (m)''' : Chaldéen.<br>
'''Χαλκηδών, -όνος (nom commun) (f)''' : Chalcédoine.<br>
'''Χαλκίς, -δος (nom commun) (f)''' Chalcis.<br>
'''Χάλυψ, -ϐος (nom propre) (m)''' : Chalybe.<br>
'''Χαπύ (nom propre) (m)''' : Hâpy.<br>
'''Χάρις, -τος (nom propre) (f)''' : Charite.<br>
'''Χαρίτων, -ος (nom propre) (m)''' : Chariton.<br>
'''Χάρυϐδις, -ύϐδεως (nom propre) (f)''' : Charybde.<br>
'''Χάρων, -ος (nom propre) (m)''' : Charon. (nocher des Enfers)<br>
'''Χέϐρης, -ου (nom propre) (m)''' : Toutânkhamon.<br>
'''Χείρων, -ος (nom propre) (m)''' : Chiron.<br>
'''Χένερης, -ου (nom propre) (m)''' : Khâsekhemoui.<br>
'''Χέοψ, -πος (nom propre) (m)''' : Khéops.<br>
'''Χεφρήν, -ένος (nom propre) (m)''' : Khéphren.<br>
'''Χηµία, -ας (nom propre) (f)''' : Égypte.<br>
'''Χθών, -ονός (nom propre) (f)''' : Chthon.<br>
'''Χιόνη, -ης (nom propre) (f)''' : Chioné.<br>
'''Χλόη, -ης (nom propre) (f)''' : Chloé.<br>
'''Χλωρίς, -δος (nom propre) (f)''' : Chloris (nymphe).<br>
'''Χνοῦϐις, -ύϐιδος (nom propre) (m)''' : [[wikt:Khnoum|Khnoum]].<br>
'''Χοίακ (nom propre) (m)''' : Choeac.<br>
'''Χοσρόης, -ου (nom propre) (m)''' : Chosroès.<br>
'''Χριστόφορος, -όρου (prénom) (m)''' : Christophe.<br>
'''Χριστός, -οῦ (nom commun) (m)''' : Christ.<br>
'''Χρυσάωρ, -ου (nom propre) (m)''' : Chrysaor.<br>
'''Χρύσιππος, -ίππου (nom propre) (m)''' : Chrysippe.<br>
'''Χρυσόμαλλον Δέρας (locution nominale)''' : Toison d'or.<br>
'''Χρυσόμαλλος, -ου (nom propre) (m)''' : Chrysomallos.<br>
'''Χρυσόπολις, -όλεως (nom propre) (m)''' : Üsküdar.<br>
==Ψ==
'''ψάλλω (verbe)''' : chanter.<br>
'''ψαλμός, -οῦ (nom commun) (m)''' : psaume.<br>
'''ψαλτήριον, -ίου (nom commun) (n)''' : psaltérion.<br>
'''ψάλτιγξ, -γος (nom commun) (m)''' : psaltinx.<br>
'''ψάρ, -ός (nom commun) (m)''' : étourneau.<br>
'''ψαρός, -ά, -όν (adjectif)''' : .<br>
'''ψαῦμα, -ύματος (nom commun) (n)''' : palpation.<br>
'''ψαῦσις, -ύσεως (nom commun) (f)''' : palpation.<br>
'''ψαύω (verbe)''' : palper.<br>
'''ψᾶφαξ, -άφακου (nom commun) (m)''' : Forme éolienne de ''ψῆφος''.<br>
'''ψᾶφος, -άφου (nom commun) (m)''' : Forme dorienne de ''ψῆφος''.<br>
'''ψάω (verbe)''' : Frotter, gratter.<br>
'''ψέγω (verbe)''' : Blâmer, reprocher.<br>
'''ψευδής, -ής, -ές (adjectif)''' : faux.<br>
'''ψεύδομαι (verbe)''' : mentir.<br>
'''ψεῦδος, -ύδους (nom commun) (n)''' : tromperie.<br>
'''ψευδῶς (adverbe)''' : faussement.<br>
'''ψευδώτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ψευδής''.<br>
'''ψευδώτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ψευδής''.<br>
'''ψεύδω (verbe)''' : tromper.<br>
'''ψεῦσμα, -ύσματος (nom commun) (n)''' : mensonge.<br>
'''ψεύστης, -ου (nom commun) (m)''' : menteur.<br>
'''ψεύστρα, -ας (nom commun) (f)''' : menteuse.<br>
'''ψῆγμα, -ήγματος (nom commun) (n)''' : pépite.<br>
'''ψηλάφησις, -ήσεως (nom commun) (f)''' : palpation.<br>
'''ψηλαφῶ (verbe)''' : palper.<br>
'''ψήρ, -ός (nom commun) (m)''' : Forme ionienne de ''ψάρ''.<br>
'''ψῆφος, -ήφου (nom commun) (m)''' : Galet, caillou, pierre, en particulier utilisée pour le calcul. Gemme, pierre précieuse.<br>
'''ψήχω (verbe)''' : racler.<br>
'''ψῖ (nom commun) (n)''' : psi.<br>
'''ψιλός, -ή, -όν (adjectif)''' : Nu ; simple.<br>
'''ψίξ, -χός (nom commun) (m/f)''' : Mie ; miette.<br>
'''ψιττακός, -οῦ (nom commun) (m)''' : perroquet.<br>
'''ψιχίον, -ου (nom commun) (n)''' : miette.<br>
'''ψίω (verbe)''' : .<br>
'''ψόα, -ας (nom commun) (f)''' : lombes.<br>
'''ψόγος, -ου (nom commun) (m)''' : Blâme, reproche.<br>
'''ψόλος, -ου (nom commun) (m)''' : suie.<br>
'''ψό (interjection)''' : beurk.<br>
'''ψυδρεύς, -έως (nom commun) (m)''' : trompeur.<br>
'''ψυδρός, -ά, -όν (adjectif)''' : faux.<br>
'''ψυχεινός, -ή, -όν (adjectif)''' : frais.<br>
'''ψυχή, -ῆς (nom commun) (f)''' : Âme ; papillon.<br>
'''ψύθος, -ους (nom commun) (n)''' : Forme homérique de ''ψεῦδος''.<br>
'''ψυχοπομπός, -ός, -όν (adjectif)''' : psychopompe.<br>
'''ψυχοπομπός, -οῦ (nom commun) (m)''' : psychopompe.<br>
'''ψῦχος, -ύχους (nom commun) (n)''' : fraîcheur.<br>
'''ψυχραίνω (verbe)''' : refroidir.<br>
'''ψυχρός, -ά, -όν (adjectif)''' : Froid ; glacial.<br>
'''ψυχρότατος, -άτη, -ότατον (adjectif)''' : Superlatif de ''ψυχρός''.<br>
'''ψυχρότερος, -έρη, -ότερον (adjectif)''' : Comparatif de ''ψυχρός''.<br>
'''ψυχρῶς (adverbe)''' : Froidement ; glacialement.<br>
'''ψύχω (verbe)''' : Souffler, respirer. Se refroidir.<br>
'''ψχέντ (nom commun) (m)''' : pschent.<br>
'''ψωμός, -οῦ (nom commun) (m)''' : bouchée de pain.<br>
'''Ψαπφώ, -οῦς (nom propre) (f)''' : Forme éolienne de ''Σαπφώ''.<br>
'''Ψίλαξ, -κος (nom propre) (m)''' : Psilax.<br>
'''Ψιχάρπαξ, -γος (nom propre) (m)''' : Psicharpax.<br>
'''Ψουσέννης, -ου (nom propre) (m)''' : Psousennès.<br>
'''Ψυχή, -ῆς (nom propre) (f)''' : Psyché.<br>
'''Ψωφίς, -δος (nom propre) (f)''' : Psophis.<br>
==Ω==
'''ᾠδή, -ῆς (nom commun) (f)''' : chant.<br>
'''ὠδυσάμην, - ()''' : .<br>
'''ὠθῶ (verbe)''' : pousser, forcer ; (militaire) repousser l’ennemi.<br>
'''ὠκεανός, -οῦ (nom commun) (m)''' : océan.<br>
'''ὠκέως (adverbe)''' : rapidement.<br>
'''ὠκύς, -εῖα, -ύ (adjectif)''' : rapide.<br>
'''ὠκύτατος, -άτη, -ώτατον (adjectif)''' : Superlatif de ''ὠκύς''.<br>
'''ὠκύτερος, -έρα, -ώτερον (adjectif)''' : Comparatif de ''ὠκύς''.<br>
'''ὦλαξ, ὤλακος (nom commun) (m)''' : Forme dorienne de ''αὖλαξ''.<br>
'''ὠλένη, -ης (nom commun) (f)''' : coude, coudée ; avant-bras.<br>
'''ὠμοπλάτη, -ης (nom commun) (f)''' : omoplate.<br>
'''ὦμος, ὤμου (nom commun) (m)''' : épaule.<br>
'''ὠνέομαι (verbe)''' : acheter.<br>
'''ὠνητής, -οῦ (nom commun) (m)''' : client.<br>
'''ὦνος, ὤνου (nom commun) (m)''' : prix.<br>
'''ᾠόν, -οῦ (nom commun) (n)''' : œuf.<br>
'''ὦ μέγα (nom commun) (n)''' : oméga.<br>
'''ὤρα, -ας (nom commun) (f)''' : soin ; souci.<br>
'''ὥρα, -ας (nom commun) (f)''' : heure.<br>
'''ὡραῖος, -ία, -ῖον (adjectif)''' : Qui est de la saison. Qui se fait à une époque, dans une saison déterminée. Qui est dans la fleur de l’âge.<br>
'''ὠρανός, -οῦ (nom commun) (m)''' : Forme dorienne et béotienne de ''οὐρανός''.<br>
'''ὡρεῖον, -ίου (nom commun) (n)''' : grenier.<br>
'''ὤρη, -ης (nom commun) (f)''' : Forme ionienne de ''ὤρα''.<br>
'''ὥρη, -ης (nom commun) (f)''' : Forme ionienne de ''ὥρα''.<br>
'''ὥριμος, -ος, -ον (adjectif)''' : mûr.<br>
'''ὡρολόγιον, -ίου (nom commun) (n)''' : horloge.<br>
'''ὡσαννά (interjection)''' : hosanna.<br>
'''ὦσις, ὤσεως (nom commun) (f)''' : poussée.<br>
'''ὦς, -τός (nom commun) (n)''' : Forme dorienne de ''οὖς''.<br>
'''ὡς (adverbe ; conjonction)''' : .<br>
'''ὥτερος, -έρα, -ερον (adjectif)''' : Autre forme dorienne de ''ἕτερος''.<br>
'''ὠτικός, -ή, -όν (adjectif)''' : otique.<br>
'''ὠτίον, -ίου (nom commun) (n)''' : Diminutif de '' οὖς''.<br>
'''ὠτίς, -δος (nom commun) (f)''' : outarde.<br>
'''ὦτος, ὤτου (nom commun) (m)''' : hibou.<br>
'''ὠφέλεια, -ίας (nom commun) (f)''' : Aide ; secours.<br>
'''ὠφέλημα, -ήματος (nom commun) (n)''' : .<br>
'''ὠφέλησις, -ήσεως (nom commun) (f)''' : Aide ; secours.<br>
'''ὠφελῶ (verbe)''' : Aider ; secourir.<br>
'''ὠχρός, -ά, -όν (adjectif)''' : pâle.<br>
'''ὤψ, -πός (nom commun) (f)''' : Vue ; visage.<br>
'''ὦ (particule)''' : ô.<br>
'''ὤ (interjection)''' : ah !, oh !<br>
'''Ὠγυγία, -ας (nom propre) (f)''' : Ogygie.<br>
'''Ὠγυγίη, -ης (nom propre) (f)''' : Forme ionienne de ''Ὠγυγία''.<br>
'''Ὠκεανίς, -δος (nom propre) (f)''' : Océanide.<br>
'''Ὠκεανός, -οῦ (nom propre) (m)''' : Océan.<br>
'''Ὠρανός, -οῦ (nom propre) (m)''' : Forme dorienne et béotienne de ''Οὐρανός''.<br>
'''Ὠρείθυια, -ίας (nom commun) (f)''' : Orithye (fille d'Érechthée).<br>
'''Ὠριγένης, -ου (nom propre) (m)''' : Origène.<br>
'''Ὠρομέδων, -οντος (nom propre) (m)''' : Oromédon.<br>
'''Ὧρος, Ὥρου (nom propre) (m)''' : [[wikt:Horus|Horus]].<br>
qsuqvggkdk0mcpx3u34rwliitbkz98f
Fonctionnement d'un ordinateur/Le préchargement
0
65804
772461
764659
2026-09-19T19:22:59Z
Mewtow
31375
/* Les problèmes à résoudre pour précharger une donnée/instruction */
772461
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions. Il faut dire que parler du préchargement des instructions demande de parler des algorithmes de prédiction de branchement, ce qui fera l'objet d'un chapitre ultérieur.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données.
Pour les instructions, cela revient à prédire quelles instructions seront exécutées dans le futur. Vous vous dites qu'il n'y a rien de plus simple : l'exécution des instruction est séquentielle, les instructions se suivent en mémoire, rien de bien compliqué. Mais il faut prendre en compte les branchements qui peuvent envoyer le programme n'importe où en mémoire. Et résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié.
Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les algorithmes de préchargement des données==
Le préchargement est fortement influencé par la manière dont les données sont représentées en mémoire. Le point principal est si elles sont regroupées ensemble dans un tableau ou si elles sont dispersées, et comment elles sont dispersées. Il existe ainsi des ''prefetchers'' qui marchent assez bien pour certaines structures de données spécialisées et très mal sur d'autres. Les ''prefetchers'' pour les données sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien pour les autres structures de données, comme les listes chainées ou les arbres, mais il existe des ''prefetchers'' spécialisés pour ces dernières.
===Les ''Prefetchers'' séquentiels===
Les ''prefetchers'' séquentiels préchargent les données immédiatement consécutives de la donnée venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. Avec la première, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. Autre solution : garder un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
====Les ''stream buffers''====
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les blocs. Lors d'un défaut de cache, le bloc lu/écrit par le processeur est chargé depuis la mémoire et placé dans le cache. Les blocs suivants sont eux préchargés dans le ''stream buffer''. Le ''stream buffer'' permet de précharger un certain nombre de blocs, fixe : 2, 3, 4 blocs, rarement plus. Si le processeur accède à un bloc préchargé, celui-ci sera lu directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Pas besoin de modifier la politique de remplacement du cache, en cas de préchargement inutile, par exemple. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
====L’utilisation du préchargement séquentiel pour les instructions====
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. Pour cela, le processeur contient une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
o4sos4oqo5vt8fdr0qdnoi60bheus0c
772462
772461
2026-09-19T19:23:20Z
Mewtow
31375
/* L’utilisation du préchargement séquentiel pour les instructions */
772462
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions. Il faut dire que parler du préchargement des instructions demande de parler des algorithmes de prédiction de branchement, ce qui fera l'objet d'un chapitre ultérieur.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données.
Pour les instructions, cela revient à prédire quelles instructions seront exécutées dans le futur. Vous vous dites qu'il n'y a rien de plus simple : l'exécution des instruction est séquentielle, les instructions se suivent en mémoire, rien de bien compliqué. Mais il faut prendre en compte les branchements qui peuvent envoyer le programme n'importe où en mémoire. Et résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié.
Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les algorithmes de préchargement des données==
Le préchargement est fortement influencé par la manière dont les données sont représentées en mémoire. Le point principal est si elles sont regroupées ensemble dans un tableau ou si elles sont dispersées, et comment elles sont dispersées. Il existe ainsi des ''prefetchers'' qui marchent assez bien pour certaines structures de données spécialisées et très mal sur d'autres. Les ''prefetchers'' pour les données sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien pour les autres structures de données, comme les listes chainées ou les arbres, mais il existe des ''prefetchers'' spécialisés pour ces dernières.
===Les ''Prefetchers'' séquentiels===
Les ''prefetchers'' séquentiels préchargent les données immédiatement consécutives de la donnée venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. Avec la première, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. Autre solution : garder un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
====Les ''stream buffers''====
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les blocs. Lors d'un défaut de cache, le bloc lu/écrit par le processeur est chargé depuis la mémoire et placé dans le cache. Les blocs suivants sont eux préchargés dans le ''stream buffer''. Le ''stream buffer'' permet de précharger un certain nombre de blocs, fixe : 2, 3, 4 blocs, rarement plus. Si le processeur accède à un bloc préchargé, celui-ci sera lu directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Pas besoin de modifier la politique de remplacement du cache, en cas de préchargement inutile, par exemple. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
====Le préchargement séquentiel pour les instructions====
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. Pour cela, le processeur contient une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
tsy05tekfocz5iku8uvd0qsckmezoop
772463
772462
2026-09-19T19:27:37Z
Mewtow
31375
/* Les Prefetchers séquentiels */
772463
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions. Il faut dire que parler du préchargement des instructions demande de parler des algorithmes de prédiction de branchement, ce qui fera l'objet d'un chapitre ultérieur.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données.
Pour les instructions, cela revient à prédire quelles instructions seront exécutées dans le futur. Vous vous dites qu'il n'y a rien de plus simple : l'exécution des instruction est séquentielle, les instructions se suivent en mémoire, rien de bien compliqué. Mais il faut prendre en compte les branchements qui peuvent envoyer le programme n'importe où en mémoire. Et résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié.
Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les algorithmes de préchargement des données==
Le préchargement est fortement influencé par la manière dont les données sont représentées en mémoire. Le point principal est si elles sont regroupées ensemble dans un tableau ou si elles sont dispersées, et comment elles sont dispersées. Il existe ainsi des ''prefetchers'' qui marchent assez bien pour certaines structures de données spécialisées et très mal sur d'autres. Les ''prefetchers'' pour les données sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien pour les autres structures de données, comme les listes chainées ou les arbres, mais il existe des ''prefetchers'' spécialisés pour ces dernières.
===Les ''Prefetchers'' séquentiels===
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. Avec la première, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. Autre solution : garder un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
====Les ''stream buffers''====
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les blocs. Lors d'un défaut de cache, le bloc lu/écrit par le processeur est chargé depuis la mémoire et placé dans le cache. Les blocs suivants sont eux préchargés dans le ''stream buffer''. Le ''stream buffer'' permet de précharger un certain nombre de blocs, fixe : 2, 3, 4 blocs, rarement plus. Si le processeur accède à un bloc préchargé, celui-ci sera lu directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Pas besoin de modifier la politique de remplacement du cache, en cas de préchargement inutile, par exemple. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
====Le préchargement séquentiel pour les instructions====
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. Pour cela, le processeur contient une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
sx3inf7jx54ypeb3okl917d3s6nktf2
772464
772463
2026-09-19T19:29:44Z
Mewtow
31375
772464
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les algorithmes de préchargement des données==
Le préchargement est fortement influencé par la manière dont les données sont représentées en mémoire. Le point principal est si elles sont regroupées ensemble dans un tableau ou si elles sont dispersées, et comment elles sont dispersées. Il existe ainsi des ''prefetchers'' qui marchent assez bien pour certaines structures de données spécialisées et très mal sur d'autres. Les ''prefetchers'' pour les données sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien pour les autres structures de données, comme les listes chainées ou les arbres, mais il existe des ''prefetchers'' spécialisés pour ces dernières.
===Les ''Prefetchers'' séquentiels===
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux. Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons. Avec la première, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger. Autre solution : garder un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
====Les ''stream buffers''====
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les blocs. Lors d'un défaut de cache, le bloc lu/écrit par le processeur est chargé depuis la mémoire et placé dans le cache. Les blocs suivants sont eux préchargés dans le ''stream buffer''. Le ''stream buffer'' permet de précharger un certain nombre de blocs, fixe : 2, 3, 4 blocs, rarement plus. Si le processeur accède à un bloc préchargé, celui-ci sera lu directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache. L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Pas besoin de modifier la politique de remplacement du cache, en cas de préchargement inutile, par exemple. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
====Le préchargement séquentiel pour les instructions====
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. Pour cela, le processeur contient une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
trq54dlupr80pk7ihjvsytnnf8llmcg
772465
772464
2026-09-19T19:34:18Z
Mewtow
31375
/* Les algorithmes de préchargement des données */
772465
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Pas besoin de modifier la politique de remplacement du cache, en cas de préchargement inutile, par exemple. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. Pour cela, le processeur contient une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
f5zg8s4jq1c7yvy111ek8u2wopd35gp
772466
772465
2026-09-19T19:36:45Z
Mewtow
31375
/* Les Prefetchers séquentiels */
772466
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. Pour cela, le processeur contient une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
8fr4bdsil9kkeyaiasz3tjyxpuw4qz9
772467
772466
2026-09-19T19:37:46Z
Mewtow
31375
/* Les algorithmes de préchargement des données */
772467
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice. Pour cela, le processeur contient une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
7nvjz3fi09gf4ih1lw0gl6covagikkl
772468
772467
2026-09-19T19:39:44Z
Mewtow
31375
/* Le préchargement selon les dépendances */
772468
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse. Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
tngjyq2f5sa7vg73ysrn767ycwhf4qg
772469
772468
2026-09-19T19:40:01Z
Mewtow
31375
/* Le préchargement de Markov */
772469
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
3ioktwau1lv1a3fgb8sd9o4hc9v4hag
772470
772469
2026-09-19T19:50:11Z
Mewtow
31375
/* Le préchargement adaptatif */
772470
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
===L'interaction avec la mémoire virtuelle===
Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ?
La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page.
Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel a depuis longtemps intégré un ''Next page prefetcher'' de ce type, mais il a été amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
kilf8k5p4f62b0emm1smpz83z09vku3
772471
772470
2026-09-19T19:53:12Z
Mewtow
31375
/* L'interaction avec la mémoire virtuelle */
772471
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses. La première est de prédire à l'avance quelles données/instructions seront utilisées dans le futur. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Le second point est de décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
===L'interaction avec la mémoire virtuelle===
Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ?
La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page.
Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
6vq2ghr00t4fhcsj7s4nrz5pekq3n23
772478
772471
2026-09-19T20:08:38Z
Mewtow
31375
772478
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ?
Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette ''pollution de cache''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
===L'interaction avec la mémoire virtuelle===
Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ?
La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page.
Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
akpkgesnxukx792yl1p1msh4kt4at6z
772479
772478
2026-09-19T20:08:54Z
Mewtow
31375
772479
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ?
Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
===L'interaction avec la mémoire virtuelle===
Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ?
La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page.
Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
ksjktx2bi8z26qzcnf017e6r4p4l027
772480
772479
2026-09-19T20:10:45Z
Mewtow
31375
/* Les stream buffers */
772480
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ?
Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
===L'interaction avec la mémoire virtuelle===
Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ?
La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page.
Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
6h4le4fttajhrshowbydlg5df0eyznn
772481
772480
2026-09-19T20:11:27Z
Mewtow
31375
/* Les Prefetchers séquentiels */
772481
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ?
Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===L'interaction avec la mémoire virtuelle===
Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ?
La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page.
Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine.
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
==Les algorithmes de préchargement des données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
sf7438gvlrtcql3q8bb7v39msac8f9z
772482
772481
2026-09-19T20:11:44Z
Mewtow
31375
/* Les algorithmes de préchargement des données */
772482
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ?
Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===L'interaction avec la mémoire virtuelle===
Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ?
La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page.
Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine.
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
==Les ''prefetchers'' spécialisés pour les données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===L'usage d'un cache spécialisé pour le préchargement===
L'autre solution est de précharger les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. C'est ce qui est utilisé dans le préchargement séquentiel avec les ''stream buffers'', par exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
dnl8ijv5cm2sxl15dyv4rhexxfcytkx
772483
772482
2026-09-19T20:12:45Z
Mewtow
31375
/* La pollution du cache */
772483
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ?
Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===L'interaction avec la mémoire virtuelle===
Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ?
La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page.
Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine.
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
==Les ''prefetchers'' spécialisés pour les données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
===L'usage d'un cache spécialisé pour le préchargement===
Une dernière solution précharge les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. Les ''stream buffers'' en sont un exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le tampon de flux contient des données préchargées à tort : le tampon de flux est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le ''stream buffer'', ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
2gb6sbg8qx07rf73fng8x4ft6k1fz1n
772484
772483
2026-09-19T20:13:23Z
Mewtow
31375
/* L'usage d'un cache spécialisé pour le préchargement */
772484
wikitext
text/x-wiki
En raison de la localité spatiale, il est avantageux de précharger dans le cache des données/instructions proches de celles chargées il y a peu. Ce '''préchargement''' (en anglais, ''prefetching''), peut être effectué par le programmeur, à la condition que le processeur possède une instruction de préchargement. Mais celui-ci peut aussi être pris en charge directement par le processeur, sans implication du programmeur, ce qui a de nombreux avantages. Pour ce faire, le processeur doit contenir un circuit nommé ''prefetcher'', associé au cache, qui charger l'avance des données/instructions susceptibles d’être utilisées dans un avenir proche. Qui plus est, on peut utiliser à la fois des instructions de préchargement et un ''prefetcher'' intégré au processeur : les deux solutions ne sont pas incompatibles.
Il faut savoir que le préchargement ne fonctionne pas tout à fait de la même manière pour les données et les instructions. La raison est que l'organisation des données en mémoire diffère de celle des instructions, du moins pour les structures de données usuelles. Les processeurs modernes disposent de ''prefetchers'' spécialisés pour les instructions, séparés des ''prefetchers'' spécialisés dans les données. Nous allons ici nous concentrer sur les ''prefetchers'' spécialisés dans les données, avec seulement quelques parenthèses sur les ''prefetchers'' d'instructions.
Précharger des données/instructions demande de faire plusieurs choses, à savoir : quoi précharger, quand le précharger, et où le précharger ?
Prédire quoi précharger implique de prédire quelles données/instructions seront utilisées dans le futur. Et plus précisément de prédire leur adresse. Et cette '''prédiction d'adresse''' ne se fait pas de la même manière entre instruction et données. Pour les instructions, prédire les instructions à exécuter serait très simple, s'il n'y avait pas de branchements, qui peuvent envoyer le programme n'importe où en mémoire. Résoudre le problème des branchements demande de faire ce qui s'appelle de la prédiction de branchements, qu'on verra dans un chapitre dédié. Pour les données, tout dépend de la manière dont les structures de données sont parcourues et tout dépend de la structure de donnée. Les tableaux sont la structure de donnée idéale pour le préchargement, comme on le verra plus tard, mais d'autres techniques fonctionnent pour d'autres structures de données.
Il faut ensuite décider ''quand précharger des données''. Il ne faut pas que le préchargement marche sur les pieds des accès mémoire normaux. Est-ce qu'on précharge les données dès que possible quitte à retarder un accès mémoire normal, où attend-t-on que la mémoire soit libre pour faire le préchargement. Dans le second cas, il faut décider si le préchargement en vaut la peine. Peut-être que la mémoire ne sera libre que trop tard, à un moment où précharger ne fera que gagner quelques cycles, ce qui ne vaut pas le coup comparé aux inconvénients du préchargement. Mais le cas inverse, à savoir un préchargement trop anticipé, peut aussi donner des performances inférieures. Dans le pire des cas, une donnée chargée trop en avance sera évincée du cache avant d'être utilisée. Tout cela fera l'objet d'une section dédiée dans le chapitre.
Enfin, il faut prendre en compte que la donnée préchargée va prendre la place d'une autre donnée dans le cache. Il faut que ce remplacement en vaille la peine. Si la donnée préchargée l'est à tord, elle remplacera une donnée potentiellement plus utile. Il faut absolument éviter cette '''pollution de cache'''. Pour éviter cela, il faut faire attention à où on place la donnée préchargée. Par exemple, il est possible de précharger une donnée dans le cache L3, plutôt que dans le cache L1 ou L2. Ainsi, on a plus de chance d'écraser une ligne de cache destinée à être évincée du cache. Une autre solution est de précharger la donnée/instruction en-dehors du cache, dans une structure matérielle séparée. Nous ne verrons un exemple avec les ''stream buffer'' dans la section suivante.
==Les ''Prefetchers'' séquentiels==
Les ''prefetchers'' séquentiels fonctionnent à la fois pour les instructions et les données. Ils préchargent les données/instructions immédiatement consécutives de la donnée/instruction venant tout juste d'être lue ou écrite. Ils fonctionnent bien lorsqu'on accède à des données consécutives en mémoire, ce qui arrive souvent lors de parcours de tableaux.
Dans le cas le plus simple, on précharge le bloc de mémoire qui suit immédiatement la dernière ligne chargée. L'adresse de ce bloc se calcule en additionnant la longueur d'une ligne de cache à la dernière adresse lue ou écrite. Ce qui peut être fait avec un seul bloc de mémoire peut aussi l'être avec plusieurs. Rien n’empêche de charger non pas un, mais deux ou trois blocs consécutifs dans notre mémoire cache. Mais attention : le nombre de blocs de mémoire chargés dans le cache est fixe.
[[File:Préchargement séquentiel.png|centre|vignette|upright=2|Préchargement séquentiel.]]
===Le préchargement séquentiel pour les instructions===
Le préchargement séquentiel est assez adapté au préchargement des instructions d'un programme, vu que ses instructions sont placées les unes après les autres en mémoire. Notons que par préchargement des instructions, on veut parler du chargement des instructions dans le cache d'instruction L1, à partir du cache L2. Les niveaux de cache en-dessous n'utilisent pas vraiment de préchargement d'instruction. Le préchargement séquentiel des instructions n'est pas une technologie nouvelle. Dès les années 60, des utilisations commerciales existaient. Par exemple, l'ordinateur IBM System/360 Model 91 gérait le préchargement séquentiel des instructions.
Le seul bémol est qu'un ''prefetcher'' purement séquentiel gère mal les branchements. Le branchement envoie le programme à une adresse qui peut être n'importe où dans le programme, pas forcément dans la ligne de cache suivante. Les techniques pour résoudre ce problème sont assez complexes, sans compter qu'elles demandent d'utiliser de la prédiction de branchement, chose que nous n'avons pas encore abordé dans ce cours.
[[File:Branchements et préchargement séquentiel.png|centre|vignette|upright=2|Branchements et préchargement séquentiel.]]
===L'interaction avec la mémoire virtuelle===
Le préchargement séquentiel interagit avec la mémoire virtuelle d'une manière qui n'est pas évidente. Concentrons-nous sur l'interaction avec la mémoire virtuelle de type "pagination". Imaginons que le programme exécuté manipule un tableau, suffisamment grand pour utiliser plusieurs pages mémoires. Il arrive alors que les données à précharger soient dans une autre page mémoire. Et c'est une situation assez problématique. A quelle adresse mémoire se trouve cette page suivante ? Est-ce que la page mémoire est dans la mémoire RAM ? Que faire si elle est sur le disque dur ? Comment le ''prefetcher'' doit réagir suivant la réponse à ces questions ?
La solution la plus simple est de ne pas se poser ces questions. Le ''prefetcher'' ne précharge pas la page suivante, le préchargement séquentiel est limité à l'intérieur d'une page. L'implémentation matérielle est très simple : il suffit d'ajouter un circuit qui détecte quand on les adresses à) précharger dépassent la limite d'une page. Le circuit prévient quand c'est le cas, et le préchargement est alors stoppé. Détecter ces adresses limites est assez simple, quand on connait la taille d'une page, il faut juste vérifier si l'adresse est alignée sur 4 kibioctets, 2 mébioctets, ou autre suivant la taille de la page.
Une autre solution précharge la page suivante et continue à précharger les données/instructions dans celle-ci. Le ''prefetcher'' détecte quand il s'approche de la fin d'une page, et déclenche la lecture de la suivante quand c'est le cas. La lecture passe par la TLB, mais peut déclencher un défaut de page. Intel et AMD ont depuis longtemps intégré un '''''Next page prefetcher''''' de ce type. Intel l'a amélioré dans la microarchitecture Redwood Cove. Le ''Next page prefetcher'' est alors devenu capable de lire non pas seulement la page suivante, mais les deux pages suivantes, si le trafic mémoire le permet. Les deux pages préchargées étaient mémorisées dans le cache L3, histoire de ne pas remplacer des données utiles dans le cache L1. Il faut dire que le cache L1 ne fait que quelques pages, moins d'une dizaine/vingtaine.
===Les ''stream buffers''===
L'implémentation du préchargement séquentiel peut se faire de nombreuses façons. Mais la plus simple et la plus commune est celle des '''''stream buffers'''''. Elle utilise une mémoire FIFO séparée du cache, appelée le ''stream buffer'', dans laquelle sont préchargés les lignes de cache. Lors d'un défaut de cache, la ligne de cache lue/écrite par le processeur est chargé depuis la mémoire et placé dans le cache. Les lignes de cache suivantes sont préchargées dans le ''stream buffer''. Si le processeur accède à une ligne de cache préchargée, elle sera lu/écrite directement depuis le ''stream buffer'', pas depuis la mémoire ou le cache.
L'usage de ''stream buffer'' est assez simple et n'entraine pas de modifications substantielles du cache, ce qui est un avantage certain. Un autre avantage est que les données préchargées ne "polluent pas le cache". L'expression veut dire que des données préchargées à tord ne remplacent pas des lignes de cache utiles. Il n'y a donc pas besoin de modifier la politique de remplacement des lignes de cache, pour gérer les préchargements foireux. De plus, le processeur peut avoir plusieurs ''stream buffer'', ce qui permet d'effectuer du préchargement pour plusieurs défauts de cache. Un défaut de cache peut remplir un ''stream buffer'', puis un autre défaut en remplir un autre, etc. Les performances sont alors d'autant plus grandes que le nombre et la taille des ''stream buffer'' sont élevés.
La toute première technique de ce genre est illustrée ci-dessous. Elle était pensée pour précharger des données dans le cache L1 d'un processeur, que ce soit pour précharger des données de la RAM dans le cache, ou pour précharger des données du cache L2 vers le cache L1. Mais cette technique s'adapte à toute forme de préchargement et à toute hiérarchie mémoire. On peut l'utiliser à tous les niveaux de la hiérarchie mémoire. Une version améliorée précharge les données dans le ''stream buffer'' non pas lors d'un défaut de cache, mais lors de chaque accès mémoire : tout accès à une donnée charge la donnée suivante dans le ''stream buffer''. Mais nous en reparlerons plus bas, dans la section sur les évènements qui déclenchent le préchargement.
[[File:CachePrefetching StreamBuffers.png|centre|vignette|upright=2|Technique des ''stream buffers''.]]
===Le préchargement adaptatif===
Le préchargement séquentiel ne fonctionne que pour des accès à des adresses consécutives, pour des données avec une bonne localité spatiale. Pour les autres types d'accès, l'utilisation d'un ''prefetcher'' séquentiel est généralement contreproductive. Pour limiter la casse, les ''prefetchers'' évolués sont capables de distinguer les accès séquentiels et les accès problématiques. Cette détection peut se faire de deux façons.
* Avec la première solution, le ''prefetcher'' calcule une moyenne du nombre de blocs préchargés qui ont été utiles, à partir des n derniers blocs préchargés. En clair, il calcule le rapport entre le nombre de blocs qu'il a préchargés dans le cache et le nombre de ces blocs qui ont été accédés. Si jamais ce rapport diminue trop, cela signifie que l'on n’a pas affaire à des accès séquentiels : le ''prefetcher'' arrête temporairement de précharger.
* Avec la seconde solution, le ''prefecther'' garde un historique des derniers accès mémoires pour vérifier s'ils accèdent à des adresses consécutives. Le processeur peut décider de désactiver temporairement le préchargement si jamais le nombre de blocs préchargés utilement tombe trop près de zéro.
==Les ''prefetchers'' spécialisés pour les données==
Les ''prefetchers'' séquentiels sont surtout adaptés à l'utilisation de tableaux (des ensembles de données consécutives de même taille et de même type). Ils profitent du fait que ces tableaux sont souvent accédés case par case. Ils marchent un peu moins bien quand les accès à un tableau ne se font pas case par case. Et ils marchent encore moins bien pour les autres structures de données, comme les listes chainées ou les arbres. Mais il existe des ''prefetchers'' spécialisés pour ces dernières, et nous allons les voir dans cette section.
===Les accès par enjambées===
Les '''accès par enjambées''' (ou ''stride access'') se font sur des données séparées par une distance constante k. Ils surviennent quand un programmeur utilise certaines structures de données, à savoir : des tableaux de structures/objets, des tableaux multidimensionnels. Un cas classique est celui d'une matrice, à savoir un tableau de nombres rectangulaire avec N lignes et M colonnes. Les matrices sont souvent stockées dans un tableau unique, où les nombres sont mémorisés ligne par ligne. Les accès qui se font ligne par ligne ne font que parcourir le tableau en passant d'une case à la suivante. Cependant, les parcours qui se font colonne par colonne, font de grandes enjabées dans la mémoire RAM.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
Avec ce genre d'accès, un ''prefetcher'' séquentiel charge des données inutiles, ce qui est synonyme de baisse de performances. Mais certains ''prefetchers'' gèrent de tels accès à la perfection. Cela ne rend pas ces accès aussi rapides que des accès à des blocs de mémoire consécutifs, vu qu'on gâche une ligne de cache pour n'en utiliser qu'une petite portion, mais cela aide tout de même beaucoup.
Ces prefetchers conservent un historique des accès mémoires effectués récemment dans un cache : la '''table de prédiction de références''' (''reference prediction table''). Chaque ligne de cache associe à une instruction toutes les informations nécessaires pour prédire quelle sera la prochaine adresse utilisée par celle-ci. Elle stocke notamment la dernière adresse lue/écrite par l'instruction, l’enjambée, ainsi que des bits qui indiquent la validité de ces informations. L'adresse de l'instruction est dans le tag de la ligne de cache.
Pour prédire la prochaine adresse, il suffit d'ajouter la longueur d’enjambée à l'adresse à lire ou écrire. Cette technique peut être adaptée pour précharger non seulement la prochaine adresse, mais aussi les n adresses suivantes, la énième adresse ayant pour valeur : adresse + n × enjambée. Évidemment, ces adresses à précharger ne peuvent pas être lues ou écrites simultanément depuis la mémoire. On doit donc les mettre en attente, le temps que la mémoire soit libre. Pour cela, on utilise un tampon de préchargement, qui stocke des requêtes de lecture ou d'écriture fournies par l'unité de préchargement.
L'algorithme de gestion des enjambées doit détecter les enjambées, déterminer leur taille de l’enjambée et précharger un ou plusieurs blocs. Détecter les enjambées et déterminer leur taille peut se faire simultanément de différentes manières. La première considère que si une instruction effectue deux défauts de cache à la suite, elle effectue un accès par enjambées. Il s'agit là d'une approximation grossière, mais qui ne fonctionne pas trop mal. Avec cette méthode, une ligne de cache de la table de prédiction de référence peut avoir deux états : un état où l'instruction n'effectue pas d'accès par enjambées, ainsi qu'un second état pour les instructions qui effectuent des accès par enjambées. La première fois qu'une instruction effectue un défaut de cache, une entrée lui est allouée dans la table de prédiction de références. La ligne est initialisée avec une enjambée inconnue en état "préchargement désactivé". Lors du second accès, la ligne de cache est mise à jour en état "préchargement activé". Le ''prefetcher'' considère que la distance entre les deux adresses (celle du premier accès et celle du second) est l'enjambée, cette distance se calculant avec une simple soustraction.
[[File:Calcul de l’enjambée.png|centre|vignette|upright=2|Calcul de l’enjambée.]]
Néanmoins, cet algorithme voit souvent des accès par enjambées là où il n'y en a pas. Une solution à cela consiste à attendre un troisième accès avant de commencer le préchargement, afin de vérifier si l'enjambée calculée est la bonne. Lorsque l'instruction effectue son premier défaut de cache, l'entrée est initialisée dans l'état ''no prefetch''. Lors du défaut de cache suivant, l’enjambée est calculée, mais le préchargement ne commence pas : l'entrée est placée dans l'état ''init''. C'est lors d'un troisième défaut de cache que l’enjambée est recalculée, et comparée avec l’enjambée calculée lors des deux précédents défauts. Si les deux correspondent, un accès par enjambées est détecté, et le préchargement commence. Sinon, l'instruction n'effectue pas d'accès par enjambées : on place l'entrée en état ''no prefetch''.
[[File:Calcul amélioré de l’enjambée, partie 2.png|centre|vignette|upright=2|Calcul amélioré de l’enjambée, partie 2.]]
On peut améliorer l'algorithme précédent pour recalculer l’enjambée à chaque accès mémoire de l'instruction, et vérifier si celui-ci a changé. Si un changement est détecté, la prédiction avec enjambée est certainement fausse et on ne précharge rien. Pour que cet algorithme fonctionne, on doit ajouter un quatrième état aux entrées : « transitoire » (''transient''), qui stoppe le préchargement et recalcule l’enjambée.
[[File:Recalcul du préchargement à chaque défaut de cache.png|centre|vignette|upright=2|Recalcul du préchargement à chaque défaut de cache.]]
===Le préchargement selon les dépendances===
Certaines applications ont besoin de structures de données qui permettent de supprimer ou d'ajouter un élément rapidement. On peut notamment citer les listes, les arbres et les graphes. Dans ces structures de données alternatives aux tableaux, les données sont souvent dispersées dans la mémoire. Pour faire le lien entre les données, chacune d'entre elles sera stockée avec les adresses des données suivantes ou précédentes. Les ''prefetechers'' précédents fonctionnent mal avec ces structures de données, où les données ne sont pas placées à intervalle régulier en mémoire. Cela dit, il n'existe des techniques de préchargement adaptées pour ce genre de structures de données.
La première de ces techniques a reçu le nom de '''préchargement selon les dépendances''' (''dependence based prefetching''). Elle ne donne de bons résultats que sur des listes. Prenons un exemple : une liste simplement chainée, une structure où chaque donnée indique l'adresse de la suivante. Pour lire la donnée suivante, le processeur doit récupérer son adresse, qui est placée à côté de la donnée actuelle. Puis, il doit charger tout ou partie de la donnée suivante dans un registre. Pour résumer, on se retrouve avec deux lectures : la première récupère l'adresse et l'autre l'utilise. Dans ce qui va suivre, je vais identifier ces deux instructions en parlant d'instruction productrice (celle qui charge l'adresse) et consommatrice (celle qui utilise l'adresse chargée).
{|
|[[File:Instruction productrice.jpg|vignette|Instruction productrice.]]
|[[File:Instruction comsommatrice.jpg|vignette|Instruction consommatrice.]]
|}
Avec le préchargement selon les dépendances, le processeur mémorise si deux instructions ont une dépendance producteur-consommateur dans un cache : la '''table de corrélations'''. Chaque ligne de celle-ci stocke les adresses du producteur et du consommateur. Reste que ces corrélations ne sortent pas de la cuisse de Jupiter. Elles sont détectées lors de l’exécution d'une instruction consommatrice. Pour toute lecture, le processeur vérifie si la donnée à lire a été chargée par une autre instruction : si c'est le cas, l'instruction est consommatrice.
La vérification est faite grâce à une table de correspondances entre la donnée lue et l'adresse de l'instruction (le ''program counter'') : la '''fenêtre de producteurs potentiels'''. Lors de l’exécution d'une instruction, il vérifie si l'adresse à lire est dans la fenêtre de producteurs potentiels : si c'est le cas, c'est qu'une instruction productrice a chargé l'adresse, et que l'instruction en cours est consommatrice. L'adresse des instructions productrice et consommatrice sont alors stockées dans la table de corrélations. À chaque lecture, le processeur vérifie si l'instruction est productrice en regardant le contenu de la table de corrélations. Dès qu'une instruction détectée comme productrice a chargé son adresse, le processeur précharge les données de l'instruction consommatrice associée. Lorsqu'elle s’exécutera quelques cycles plus tard, la donnée aura déjà été lue depuis la mémoire.
===Le préchargement de Markov===
Pour les structures de données plus évoluées, comme des arbres ou des graphes, la technique précédente ne marche pas très bien. Avec ces types de données, chaque donnée a plusieurs successeurs, ce qui fait qu'une instruction consommatrice ne va pas toujours consommer la même adresse.
Pour gérer cette situation, on doit utiliser des ''prefetchers'' plus évolués, comme des '''''prefetchers'' de Markov'''. Ils fonctionnent comme les précédents, sauf que la table de corrélations permet de mémoriser plusieurs correspondances, plusieurs adresses de successeurs. Dans certains ''prefetchers'', toutes les adresses des successeurs sont préchargées. Mais sur d'autres, le ''prefetcher'' se débrouille pour prédire quelle sera la bonne adresse du successeurs. Pour cela, le ''prefetcher'' calcule, pour chaque adresse, la probabilité qu'elle soit accédée. À chaque lecture ou écriture, les probabilités sont mises à jour. Seule l'adresse de plus forte probabilité est préchargée.
Vu que la mémoire ne peut précharger qu'une seule donnée à la fois, certaines adresses sont mises en attente dans une mémoire tampon de préchargement. Lors du préchargement, le ''program counter'' de l'instruction qui initiera le préchargement sera envoyé à la table de correspondance. Cette table fournira plusieurs adresses, qui seront mises en attente dans le tampon de préchargement avant leur préchargement. L'ordre d'envoi des requêtes de préchargement (le passage de la mémoire tampon au sous-système mémoire) est déterminé par les probabilités des différentes adresses : on précharge d'abord les adresses les plus probables.
===Le préchargement par distance===
Le gain apporté par les ''prefetchers'' vus auparavant est appréciable, mais ceux-ci fonctionnent mal sur des accès cycliques ou répétitifs, certes rares dans le cas général, mais présents à foison dans certaines applications. Ils apparaissent surtout quand on parcourt plusieurs tableaux à la fois. Pour gérer au mieux ces accès, on a inventé des ''prefetchers'' plus évolués, capables de ce genre de prouesses.
[[File:Accès mémoire cyclique sans enjambée.jpg|centre|vignette|upright=2|Accès mémoire cyclique sans enjambée.]]
Le '''préchargement par distance''' (''distance prefetching''), une adaptation du ''prefetcher'' du Markov, est un de ces ''prefetchers''. Celui-ci n'utilise pas les adresses, mais les différences entre adresses accédées de manière consécutive, qui sont appelées des deltas. Ces deltas se calculent tout simplement en soustrayant les deux adresses. Ainsi, si j'accède à une adresse A, suivie par une adresse B, le préchargement par distance calculera le delta B - A, et l'utilisera pour sélectionner une entrée dans la table de correspondances. La table de correspondances est toujours structurée autour d'entrées, qui stockent chacune plusieurs correspondances, sauf qu'elle stocke les deltas. Cette table permet de faire des prédictions du style : si le delta entre B et A est de 3, alors le delta entre la prochaine adresse sera soit 5, soit 6, soit 7. L'utilité du ''prefetcher'' de Markov, c'est que la même entrée peut servir pour des adresses différentes.
===Le Tampon d’historique global===
Les techniques vues plus haut utilisent toutes une sorte de table de correspondances. L'accès à la table s'effectue soit en envoyant le ''program counter'' de l'instruction en entrée (préchargement par enjambées), soit l'adresse lue, soit les différences entre adresses. Ce qui est envoyé en entrée sera appelé l''''index''' de la table, dans la suite de cette partie. Cette table stocke une quantité limitée de données, tirées de l'historique des défauts de cache précédents. En somme, la table stocke, pour chaque index, un historique des défauts de cache associés à l'index. Dans les techniques vues précédemment, chaque table stocke un nombre fixe de défauts de cache par index : le ''one block lookahead'' stocke une adresse par instruction, le ''stride'' stocke une enjambée et une adresse pour chaque instruction, le préchargement de Markov stocke une ou plusieurs adresses par instruction, etc. Dit autrement, l'historique a une taille fixe.
Vu que cette quantité est fixe, elle est souvent sous-utilisée. Par exemple, le préchargement de Markov limite le nombre d'adresses pour chaque instruction à 4, 5, 6 suivant le ''prefetcher''. Certaines instructions n'utiliseront jamais plus de deux entrées, tandis que le nombre de ces entrées n'est pas suffisant pour d'autres instructions plus rares. La quantité d'informations mémorisée pour chaque instruction est toujours la même, alors que les instructions n'ont pas les mêmes besoins : c'est loin d'être optimal. De plus, le nombre de défauts de cache par index limite le nombre d'instructions ou d'adresses qui peuvent être prédites. De plus, il se peut que des données assez anciennes restent dans la table de prédiction, et mènent à de mauvaises prédictions : pour prédire l'avenir, il faut des données à jour. Pour éviter ce genre de défauts, les chercheurs ont inventé des ''prefetchers'' qui utilisent un tampon d’historique global (''global history buffer''). Celui-ci permet d’implémenter plusieurs techniques de préchargement. Les techniques précédentes peuvent s'implémenter facilement sur ces ''prefetchers'', mais sans les défauts cités au-dessus.
Ces ''prefetchers'' sont composés de deux sous-composants. Premièrement, on trouve une mémoire tampon de type FIFO (''First In, First Out'') qui mémorise les défauts de cache les plus récents : l''''historique global'''. Pour chaque défaut de cache, la mémoire FIFO mémorise l'adresse lue ou écrite dans une entrée. Pour effectuer des prédictions crédibles, ces défauts de cache sont regroupés suivant divers critères : l'instruction à l'origine du défaut, par exemple. Pour cela, les entrées sont organisées en liste chainée : chaque entrée pointe sur l'entrée suivante qui appartient au même groupe. On peut voir chacune de ces listes comme un historique dédié à un index : cela peut être l'ensemble des défauts de cache associés à une instruction, l'ensemble des défauts de cache qui suivront l'accès à une adresse donnée, etc. Généralement, les instructions sont triées à l'intérieur de chaque groupe dans l'ordre d'arrivée : l'entrée la plus récente contient le défaut de cache le plus récent du groupe. Ainsi, la taille de l'historique s'adapte dynamiquement suivant les besoins, contrairement aux ''prefetchers'' précédents où celui-ci tait de taille fixe.
[[File:Tampon d’historique global.jpg|centre|vignette|upright=1|Tampon d’historique global.]]
Reste que le processeur doit savoir où est l'entrée qui correspond au début de chaque liste. Pour cela, on doit rajouter une '''table de correspondances d'historiques''', qui permet de dire où se trouve l'historique associé à chaque index. Cette table de correspondances (index → historique par index) a bien sûr une taille finie. En somme, le nombre d'entrées de cette table limite le nombre d'index (souvent des instructions) gérées en même temps. Mais par contre, pour chaque instruction, la taille de l'historique des défauts de cache est variable.
[[File:Tampon d’historique global avec sa table d’index.jpg|centre|vignette|upright=2|Tampon d’historique global avec sa table d’index.]]
La table de correspondances et l'historique global sont couplés avec un '''circuit de prédiction''', qui peut utiliser chaque historique pour faire ces prédictions. Celui-ci peut aussi bien utiliser la totalité de l'historique global, que les historiques dédiés à un index. Faire une prédiction est simple demande d’accéder à la table de correspondances avec l'index adéquat : l'adresse lue ou écrite, le ''program counter'' de l'instruction d'accès mémoire, la distance entre cette adresse et la précédente, etc. Cela va alors sélectionner une liste dans l'historique global, qui sera parcourue de proche en proche par le circuit de prédiction, qui déterminera l'adresse à précharger en fonction de l'historique stocké dans la liste. Dans certains cas, l'historique global est aussi parcouru par le circuit de prédiction, mais c'est plus rare.
Ce tampon d’historique global permet d’implémenter un algorithme de Markov assez simplement : il suffit que la table d'index mémorise une correspondance adresse → début de liste. Ainsi, pour chaque adresse, on associera la liste d'adresses suivantes possibles, classées suivant leur probabilité. L'adresse au début de la liste sera la plus probable, tandis que celle de la fin sera la moins probable. Même chose pour le préchargement par distance : il suffit que l'index soit la distance entre adresse précédemment accédée et adresse couramment accédée. Dans ce cas, la liste des entrées mémorisera la suite de distances qui correspond. L'implémentation d'un préchargement par enjambées est aussi possible, mais assez complexe. Mais de nouveaux algorithmes sont aussi possibles.
===Les variantes du tampon d’historique global===
Des variantes du tampon d'historique global ont été inventées. On pourrait citer celle qui ajoute, en plus de l'historique global, des historiques locaux sous la forme de mémoires FIFO qui mémorisent les derniers accès effectués par une instruction. Plus précisément, si on dispose de n historiques locaux, chacun de ces n historiques mémorise l'historique des n dernières instructions d'accès mémoire les plus récentes. D'autres variantes, et notamment celle du '''cache d'accès aux données''', ont ajouté une seconde table d'index :
* la première table d'index prend en entrée le ''program counter'' de l'instruction à l'origine du défaut de cache ou de l'accès mémoire ;
* la seconde prend en entrée l'adresse à lire ou écrire.
[[File:Data access cache.png|centre|vignette|upright=2|Cache d'accès aux données.]]
==La pollution du cache==
Le ''prefetcher'' peut se tromper et précharger des données inutilement dans le cache. Et outre l'inutilité de charger des données qui ne servent à rien, cela éjecte aussi des données potentiellement utiles du cache. C'est le phénomène de '''pollution de cache'''. Il va de soi que limiter au maximum cette pollution du cache permet de tirer parti au maximum de la mémoire cache, reste à savoir comment. Diverses solutions existent.
===L'usage d'un ''Dirty bit''===
Avec la première solution, la donnée chargée inutilement sera sélectionnée pour remplacement lors du prochain défaut de cache. Si le cache utilise un algorithme de sélection des lignes de cache de type LRU (''Least Recently Used''), on peut la mettre directement dans l'état « utilisée la moins récemment », ou « très peu utilisée ».
===Le filtrage de cache===
Une autre solution consiste à détecter les requêtes de préchargement inutiles en sortie du ''prefetcher''. Entre les circuits d'adressage de la mémoire (ou les niveaux de cache inférieurs) et le ''prefetcher'', on ajoute un circuit de filtrage qui détecte les requêtes de préchargement visiblement inutiles et contreproductives. Les algorithmes utilisés par ce circuit de filtrage de cache varient considérablement suivant le processeur et les travaux de recherche sur le sujet sont légion.
===Le duel d’ensembles===
Des chercheurs ont inventé des techniques plus complexes, dont la plus connue est le duel d'ensembles (''set dueling''). Dans leurs travaux, ils utilisent un cache associatif à plusieurs voies. Les voies sont réparties en deux groupes : statiques ou dynamiques. Les voies statiques ont une politique de remplacement fixée une fois pour toutes :
* dans certaines voies statiques, toute ligne chargée depuis la mémoire est considérée comme la plus récemment utilisée ;
* dans les autres voies statiques, toute ligne chargée depuis la mémoire est considérée comme la moins récemment utilisée.
Les voies restantes choisissent dynamiquement si la ligne chargée est considérée comme la moins récemment utilisée ou la plus récemment utilisée. La décision se fait selon les voies statiques qui ont le plus de défauts de cache : si les voies "moins récemment utilisée" ont plus de défauts de cache que les autres, on ne l'utilise pas et inversement. Il suffit d'utiliser un simple compteur incrémenté ou décrémenté lors d'un défaut de cache dans une voie utilisant ou non l’optimisation.
===L'usage d'un cache spécialisé pour le préchargement===
Une dernière solution précharge les données non pas dans le cache, mais dans une mémoire dédiée au préchargement : un '''tampon de préchargement''', aussi appelé ''prefetch buffer'' en anglais. Les ''stream buffers'' en sont un exemple. Lors d'un accès au cache, on vérifie en parallèle si le tampon de préchargement contient la donnée demandée. Si ce n'est pas le cas, c'est que le le tampon de préchargement contient des données préchargées à tort : il est totalement vidé, et on va chercher la donnée en mémoire. Si la donnée est disponible dans le tampon de préchargement, deux solutions sont possibles : on y accède directement dans le tampon de préchargement, ou on la rapatrie dans le cache avant de relancer l'accès dans le cache.
==Quand précharger ?==
Une problématique importante est de savoir quand précharger des données. Si on précharge des données trop tard ou trop tôt, le résultat n'est pas optimal. Pour résoudre au mieux ce problème, il existe deux grandes solutions : le préchargement sur événement et le préchargement par prédiction.
Le '''préchargement par prédiction''' essaie de prédire le moment adéquat pour précharger, quitte à se tromper où à donner une réponse inadéquate. Le ''prefetecher'' décide de précharger ou non, en se basant sur l'historique des accès précédents. Il en déduit des statistiques qui permettent de savoir quand précharger. Par exemple, ils peuvent calculer le temps d'accès moyen entre un accès mémoire et un préchargement, et armer des chronomètres pour initialiser le préchargement en temps voulu. Les algorithmes pour cela sont assez variés, mais aussi assez compliqués.
Le '''préchargement sur événement''' consiste à précharger quand certains événements spéciaux ont lieu. Elle s'enclenche quand un évènement bloque le processeur, dans le sens où le programme est bloqué à un instant précis. Par exemple, on peut précharger à chaque défaut de cache, à chaque accès mémoire, lors de l’exécution d'un branchement (pour le préchargement des instructions), etc. Dans ce qui suit, nous allons surtout détailler le préchargement sur évènement.
===Le préchargement associé aux accès mémoire===
Le préchargement par évènement le plus simple est celui initié par les accès mémoire. Reste à voir quels accès mémoire prendre en compte. La technique la plus simple effectue un préchargement lors de chaque accès mémoire. L'idée est que lorsqu'on accède à un bloc de mémoire, on précharge le bloc suivant de manière systématique, lors de chaque accès mémoire. Et mine de rien, cela ne fonctionne pas trop mal. Par contre, le préchargement accède beaucoup à la RAM, parfois pour précharger des données inutiles.
Une optimisation serait de filtrer certains accès mémoires, à savoir que certains ne déclencheront pas de préchargement. Le choix des accès mémoire à filtrer doit se faire de manière judicieuse, elle doit identifier les accès mémoires inutiles. Une solution pour cela est de ne précharger que lors d'un défaut de cache. Ainsi, si j'ai un défaut de cache qui me force à charger un bloc dans le cache, le ''prefetcher'' chargera les blocs consécutifs suivants avec. On filtre alors beaucoup d'accès mémoires, et le préchargement est généralement assez efficace.
Une solution précharge non pas lors d'un défaut de cache, mais lors d'un succès de cache uniquement. A chaque accès d'une ligne de cache, l'idée est de précharger la suivante. Reste à expliquer ce qu'on veut dire avec la suivante. La ligne de cache accédée correspond à un bloc en mémoire RAM, appelons-le le bloc A. Par ligne de cache suivante, on veut dire le bloc suivant en mémoire RAM, celui situé après le bloc A.
Pour cela, on mémorise quelle est la dernière ligne de cache qui a été accédée. Il suffit d'ajouter un bit par ligne de cache, qui indique si cette ligne a été accédée lors du dernier cycle d'horloge. Il est automatiquement mis à un lors d'un accès à une ligne de cache. Il est remis à zéro dans deux conditions : lors d'un accès à une autre ligne de cache, au bout d'un certain temps. Le ''prefetcher'' se contente de charger le bloc qui suit la ligne de cache dont le bit vaut 1.
[[File:Préchargement anticipé.png|centre|vignette|upright=2|Préchargement anticipé.]]
===Le préchargement anticipé (''runahead prefetching'')===
La technique du '''préchargement anticipé''' (''runahead prefetching'') effectue du préchargement sur un défaut de cache, et seulement pour les lectures. Le défaut de cache est censé bloquer totalement le processeur, tant qu'il n'est pas résolu. Et le processeur est censé reprendre l'exécution du programme, une fois la donnée chargée dans les registres. L'idée du préchargement anticipé est que le processeur ne se bloque pas, mais continue l'exécution du programme alors qu'il ne devrait pas. Il rentre dans un mode ''runahead '' dans lequel il exécute le programme en avance.
Les instructions qui suivent la lecture s'exécutent, y compris d'autres instructions de lecture. Les instructions vont alors générer des adresses, des lectures vont être déclenchées, etc. Et le principe est que les lectures seront exécutées en mode ''runahead'', donc en avance. Les lectures en question, exécutées en mode ''runahead'' sont appelées des '''lectures anticipées'''. Les lectures anticipées lisent des données dans le cache de données, ce qui précharge les données associées.
Mais ce que fait le processeur en mode ''runahead'' est totalement annulé une fois que le défaut de cache est résolu, le processeur revient à la normale, à l'état antérieur. Une seule chose n'est pas effacée : les données et instructions préchargées dans le cache. Pour le reste, les registres sont remis à leur état antérieur. Pour cela, le processeur sauvegarde les registres en entrant en mode ''runahead'', puis les restaure une fois le défaut de cache terminée. De même, les écritures en mémoire RAM ne sont pas exécutées en mode ''runahead''. Pas question d'écrire des données potentiellement fausses en RAM.
: Notons que cette technique n'a de sens que sur des processeurs dits à émission dans l'ordre. Nous verrons dans la suite du cours ce que sont les processeurs à exécution dans l'ordre et dans le désordre, mais sachez que dès qu'un processeur a de l'exécution dans le désordre, la technique devient totalement inutile. De même certains processeurs à exécution dans l'ordre n'ont pas besoin de cette technique, s'ils gèrent les lectures non-bloquantes. En pratique, seuls quelques processeurs dits VLIW incorporent cette technique, et ils sont rares.
Un problème de cette technique est la gestion des dépendances avec les lectures. Généralement, une lecture est suivie d'instructions qui utilisent son résultat. Une lecture charge une donnée dans un ''registre de destination'', qui est lu par d'autres instructions dépendantes. En mode ''runahead'', ces instructions lisent le registre de destination, mais la valeur ne sera pas la bonne, car la lecture n'a pas encore eu lieu. En soi, cela ne pose pas de problèmes, vu que le processeur restaure son état normal en sortie du mode ''runahead''.
Un problème survient cependant quand ces instructions sont elle-mêmes être des lectures. De telles lectures prendront une adresse/opérande erronée, ce qui fait qu'elles ne liront pas la donnée correcte, mais une donnée qui n'avait pas à être lue. En conséquence, elles vont précharger des données invalides, qui n'auraient pas dû l'être. Le préchargement perd alors en efficacité. Une solution possible serait d'interrompre le mode ''runahead'' quand le processeur détecte ce genre de situation, le processeur restaure son état antérieur et attend la fin du défaut de cache.
Une solution alternative marque le registre de destination comme invalide. Lors d'un défaut de cache en lecture, le registre de destination est marqué comme invalide, et cette valeur invalide se propage d'instruction en instruction. Typiquement, dès qu'une instruction lit un registre invalide, elle marquera son registre de résultat comme invalide. Les adresses marquées comme invalides ne sont pas utilisées pour démarrer des lectures ou écritures, afin d'économiser des préchargements inutiles.
Pour marquer les registres comme invalides, il suffit de leur rajouter un bit ''invalid'' qui indique si le résultat est valide ou non. Il faut aussi gérer la propagation du statut invalide d'une instruction à l'autre. Cela ne pose aucun problème pour les instructions dont les opérandes et la destination sont des registres : si une opérande a un statut invalide, la destination l'est aussi. Pour les autres instructions, les règles pour propager les valeurs invalides d'une instruction à l'autre sont assez complexes, surtout quand on prend en compte les écritures. Il faut aussi gérer les branchements et autres, mais passons. L'essentiel est que toutes les instructions qui dépendent de la lecture, directement ou indirectement, soient marquées invalides.
Un autre problème survient dans un cas bien précis : une dépendance entre une lecture anticipée et une écriture anticipée. Le cas est celui où l'écriture anticipée écrit une donnée en mémoire, qui est lue par une lecture anticipée. Vu que l'écriture anticipée n'est pas exécutée, la lecture anticipée aura un registre de destination invalide, idem pour ses instructions dépendantes. Pourtant, c'est un cas où la lecture anticipée aurait pu s'exécuter en avance et précharger la bonne donnée. Mais les implémentations plus élaborées gèrent naturellement ce genre de cas.
Pour cela, il faut marquer les lignes de cache comme valides ou invalides, selon les opérandes des écritures. Une lecture marquera son registre de destination comme valide si la ligne de cache lue est valide, comme invalide si elle l'est ou qu'elle déclenche un défaut de cache. Une autre solution est de détourner les écritures dans un cache séparé, spécifique au mode ''runahead'', dans lequel les écritures en mode ''runahead'' écrivent.
: Notons que cette technique demande dans l'idéal d'utiliser des techniques de prédiction de branchement et les circuits associés, qui sont censés être vus dans les chapitres ultérieurs.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les mémoires cache
| prevText=Les mémoires cache
| next=Le Translation Lookaside Buffer
| nextText=Le Translation Lookaside Buffer
}}
</noinclude>
7db960g5rgnsb7wzvllovdiqx9gn6fh
Fonctionnement d'un ordinateur/Les processeurs superscalaires
0
65956
772415
771982
2026-09-19T15:51:45Z
Mewtow
31375
/* Les décodeurs d'instructions superscalaires */
772415
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ss0uo7wo3ivar6j6d34ix131vxs2u3m
772416
772415
2026-09-19T15:59:43Z
Mewtow
31375
/* Le décodage superscalaire large */
772416
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur RISC dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ngodxqwqgvnmjgjvafzsk4d3n23kbm4
772417
772416
2026-09-19T16:02:00Z
Mewtow
31375
/* Les décodeurs d'instructions superscalaires */
772417
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 6, voire 8 micro-opérations simultanément. Aller au-delà ne sert pas à grand-chose.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pas6nofwotl1pxdwd1a53kcs7w0kx5m
772418
772417
2026-09-19T16:03:54Z
Mewtow
31375
/* L'implémentation d'un processeur superscalaire */
772418
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
==L'implémentation d'un processeur superscalaire==
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
tbmknj581sfp2z0tazp1lx018b279pm
772419
772418
2026-09-19T17:01:24Z
Mewtow
31375
/* L'implémentation d'un processeur superscalaire */
772419
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeur superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ggj1o8zi5d9nrs9z50w67mvkwkz6tb9
772420
772419
2026-09-19T17:02:02Z
Mewtow
31375
772420
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeur superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
410ggnp79nkpaf54qu3e1eml0l66tv9
772421
772420
2026-09-19T17:02:14Z
Mewtow
31375
/* Les processeurs superscalaires étroits et larges */
772421
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
c18xellp1xtfxsk0m85n39giz5kkwur
772422
772421
2026-09-19T17:02:41Z
Mewtow
31375
/* Les différents types de CPU à émission multiple */
772422
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
e48wi3ttofrgkc2gmttx70x8kt63664
772423
772422
2026-09-19T17:03:50Z
Mewtow
31375
/* L'étape de chargement superscalaire */
772423
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
===L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
s91uc5yb5enlur88gufrx67vm20818u
772424
772423
2026-09-19T17:03:56Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large= */
772424
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
87hi19c2goxpxdfzmyciteoxwpxoivt
772425
772424
2026-09-19T17:04:26Z
Mewtow
31375
/* Les processeurs superscalaires étroits et larges */
772425
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps. Pour cela, dupliquer l'unité de chargement est parfaitement possible, à condition de dupliquer aussi les ports du cache d'instruction. Mais dans ce cas, le ''program counter'' doit générer deux adresses, au mieux consécutives, au pire prédites par l'unité de prédiction de branchement. L'implémentation est alors encore très complexe.
Une autre solution se contente de doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger deux instructions de 64 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Si les instructions sont de longueur fixe, cela charge deux instructions à la fois. Pour un processeur à triple émission, il faut tripler la taille du bus, quadrupler pour un processeur quadruple émission, etc.
Tout n'est pas si simple, quelques subtilités font qu'on doit ajouter des circuits en plus pour corriger les défauts peu intuitifs de cette implémentation naïve. Par exemple, gérer les instructions de taille variable est un peu complexe avec cette solution. Mais l'implémentation est bien plus simple qu'en doublant les ports de lecture du cache d'instruction. Nous détaillerons les unités de chargement superscalaires dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
20lqh1pa3mvshi0bwfqkugwte4bkkh1
772426
772425
2026-09-19T17:21:12Z
Mewtow
31375
/* L'impact de la superscalarité sur le front-end */
772426
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit peut simplement charger un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. L'unité de prédiction de branchement n'est pas impactée, le cache d'instruction subit quelques modifications mineures.
Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Une solution possible est de charger les instructions de destination d'un branchement. En clair, On charge P instructions consécutives, un branchement, et les M instructions vers lesquelles saute le branchement. Mais cela demande de profondes modifications du cache d'instruction, qui doit charger des blocs d'instructions provenant de deux adresses différentes : l'une fournie par le ''program counter'', l'autre par l'unité de prédiction de branchement. Pire que cela, il faut aussi gérer des situations où un bloc de N instructions contient plusieurs branchements. L'unité de prédiction de branchement doit alors prédire plusieurs branchements par cycle...
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
twdmixog9zsluq4zu4a8u696rcak9ni
772427
772426
2026-09-19T17:21:43Z
Mewtow
31375
/* L'impact de la superscalarité sur le front-end */
772427
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit peut simplement charger un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. L'unité de prédiction de branchement n'est pas impactée, le cache d'instruction subit quelques modifications mineures.
Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Une solution possible est de charger les instructions de destination d'un branchement. En clair, On charge P instructions consécutives, un branchement, et les M instructions vers lesquelles saute le branchement. Mais cela demande de profondes modifications du cache d'instruction, qui doit charger des blocs d'instructions provenant de deux adresses différentes : l'une fournie par le ''program counter'', l'autre par l'unité de prédiction de branchement. Pire que cela, il faut aussi gérer des situations où un bloc de N instructions contient plusieurs branchements. L'unité de prédiction de branchement doit alors prédire plusieurs branchements par cycle... Mais nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
bo0tcxhke0dx11z6xzihwtk6bhpr05j
772428
772427
2026-09-19T17:26:04Z
Mewtow
31375
/* L'impact de la superscalarité sur le front-end */
772428
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit peut simplement charger un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. L'unité de prédiction de branchement n'est pas impactée, le cache d'instruction subit quelques modifications mineures. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ber56ib0dcq2q7hsjpk3chkba0izjdj
772429
772428
2026-09-19T17:26:33Z
Mewtow
31375
/* L'impact de la superscalarité sur le front-end */
772429
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. L'unité de prédiction de branchement n'est pas impactée, le cache d'instruction subit quelques modifications mineures. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
aeywwwdvmb1n1gc8poane1vavp5a401
772430
772429
2026-09-19T17:26:50Z
Mewtow
31375
/* L'impact de la superscalarité sur le front-end */
772430
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire doit pouvoir charger plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
bqgbz6na5ai30h2gwn0iuyazm1rf2pm
772431
772430
2026-09-19T17:43:01Z
Mewtow
31375
/* L'impact de la superscalarité sur le front-end */
772431
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
2n0zuydf5uh3y99n480i69m3onx24a3
772432
772431
2026-09-19T17:43:46Z
Mewtow
31375
/* L'impact de la superscalarité sur le chemin de données */
772432
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures. Quant au décodeur, la solution la plus fréquemment utilisée utiliser plusieurs décodeurs. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===L'étape de chargement superscalaire===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Les décodeurs d'instructions superscalaires===
Un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
38mm0fme6robmhp3gxw4lovoxj75o4s
772433
772432
2026-09-19T17:49:54Z
Mewtow
31375
/* Le séquenceur d'un processeur superscalaire */
772433
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
====Le chargement et le décodage parallèle des instructions====
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion : une optimisation du décodage parallèle====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
====L'étape de chargement d'un CPU superscalaire large====
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
====Le décodage superscalaire large====
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. On peut par exemple utiliser un petit nombre de décodeurs et compenser avec un cache de µops. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeur. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
1xu6g2tlwumla1x9hh8ri97wszm3c7h
772434
772433
2026-09-19T17:55:43Z
Mewtow
31375
/* Le séquenceur d'un processeur superscalaire large */
772434
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
====Le chargement et le décodage parallèle des instructions====
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion : une optimisation du décodage parallèle====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Un bloc de 8, 16, 32 octets contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
i62h8k9af868j68z760eeairalphq59
772435
772434
2026-09-19T17:56:24Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large */
772435
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
====Le chargement et le décodage parallèle des instructions====
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion : une optimisation du décodage parallèle====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Une solution plus performante charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
1q9k1mg48klavi6ircufxqul6cr8o6v
772436
772435
2026-09-19T17:56:51Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large */
772436
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
====Le chargement et le décodage parallèle des instructions====
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion : une optimisation du décodage parallèle====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''fron-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements ! Tout cela pour dire que rares sont les processeurs qui implémentent cette technique.
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
jgh2xvpoza3ue7uc3f2gxquqbud1xfv
772437
772436
2026-09-19T17:57:14Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large */
772437
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
====Le chargement et le décodage parallèle des instructions====
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion : une optimisation du décodage parallèle====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''fron-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Les processeurs Atom d'Intel utilisent une solution qui couple prédiction de branchement et décodage parallèle. Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pvy0sf26zdk1sh13bfwd2of6sx3yy9l
772438
772437
2026-09-19T17:58:54Z
Mewtow
31375
/* Le décodage superscalaire large */
772438
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
====Le chargement et le décodage parallèle des instructions====
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion : une optimisation du décodage parallèle====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''fron-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
c7xxq4m0j5ui3zkm744kto2arkxwvwb
772439
772438
2026-09-19T17:59:24Z
Mewtow
31375
/* Le chargement et le décodage parallèle des instructions */
772439
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement et le décodage parallèle des instructions===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
====La macro-fusion : une optimisation du décodage parallèle====
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''fron-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
gviaqqiol3k1b38ic5lb9k0kl9b3ok4
772440
772439
2026-09-19T17:59:35Z
Mewtow
31375
/* La macro-fusion : une optimisation du décodage parallèle */
772440
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement et le décodage parallèle des instructions===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''fron-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
7hsk520o199i98ukj4kzpf567ow99hy
772441
772440
2026-09-19T18:01:18Z
Mewtow
31375
/* L'unité de renommage superscalaire */
772441
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement et le décodage parallèle des instructions===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''fron-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2.5|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
jc0wzvrouzrrq1023n0rd6dxf0c2u5m
772442
772441
2026-09-19T18:01:40Z
Mewtow
31375
/* L'émission multiple des micro-opérations flottantes */
772442
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement et le décodage parallèle des instructions===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''fron-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
8hxw4xk4l1hfi4hs79hntghaam7gr1e
772443
772442
2026-09-19T18:05:42Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large */
772443
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement et le décodage parallèle des instructions===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur.
Découper un bloc en instructions est trivial avec des instructions de longueur fixe, mais plus compliqué avec des instructions de taille variable. Il est cependant possible de s'en sortir avec deux solutions distinctes. La première solution utilise les techniques de prédécodage vues dans le chapitre sur les caches, à savoir que le découpage d'une ligne de cache est réalisé lors du chargement dans le cache d’instruction. Une autre solution améliore le circuit de détection des tailles d'instruction vu dans le chapitre sur l'unité de chargement. Avec la seconde solution, cela prend parfois un étage de pipeline entier, comme c'est le cas sur les processeurs Intel de microarchitecture P6.
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
q87mhbil1zrk0lru1z5co56uh73y65u
772444
772443
2026-09-19T18:06:50Z
Mewtow
31375
/* Le chargement et le décodage parallèle des instructions */
772444
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement et le décodage parallèle des instructions===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
ks50g8rfdk9z2ha9hqpv32bu8uzo73p
772445
772444
2026-09-19T18:07:16Z
Mewtow
31375
/* Le chargement et le décodage parallèle des instructions */
772445
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas. Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc. La majorité des processeurs superscalaires font ainsi, même de nos jours.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
j19unr0m215e7rq3tviz6wc1gta1vph
772446
772445
2026-09-19T18:08:02Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large */
772446
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pou47ui5q2wtanctm8xbckirzfk5mg1
772447
772446
2026-09-19T18:11:19Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large */
772447
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
qpnf4at9hvqnllh79hqdlhav4rzitsy
772448
772447
2026-09-19T18:12:07Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large */
772448
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=2|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=2|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
rskh9ezntdfe1t4bp6o2f2hg72mtk2d
772449
772448
2026-09-19T18:12:46Z
Mewtow
31375
/* L'émission multiple des micro-opérations entières */
772449
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
===Les ''front-end'' étroits et larges===
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
1kn6q1vvh3njfl3bnt70z7erbgavwzt
772450
772449
2026-09-19T18:13:22Z
Mewtow
31375
/* Les front-end étroits et larges */
772450
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
lkwbvwcqoyi6rgd45ss8tk7ss4hy76g
772451
772450
2026-09-19T18:14:15Z
Mewtow
31375
/* Le décodage superscalaire large */
772451
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Une implémentation classique charge N instructions consécutives pour alimenter N décodeurs. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
t407v9vp3f8dl6lti56nylq5wiob2re
772453
772451
2026-09-19T18:24:52Z
Mewtow
31375
/* Le décodage parallèle des instructions */
772453
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
na3avsgfsygsi48eaojpggarwemkji8
772454
772453
2026-09-19T18:37:43Z
Mewtow
31375
/* Les unités d'émission/renommage d'un processeur superscalaire */
772454
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
bk5tv6mdzvjj0ol4f7j9m1hy8vofynb
772455
772454
2026-09-19T18:38:29Z
Mewtow
31375
/* Les unités d'émission/renommage d'un processeur superscalaire */
772455
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
r20l5bkwzpfhyuqhc5jw27cotzwvig0
772456
772455
2026-09-19T18:39:10Z
Mewtow
31375
/* Les unités d'émission/renommage d'un processeur superscalaire */
772456
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Typiquement, la présence d'un cache de µops permet ce genre de fantaisies.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
pxnxxg2gfd5bqut8ha6w8pbhusbm6fq
772457
772456
2026-09-19T18:44:26Z
Mewtow
31375
/* Les unités d'émission/renommage d'un processeur superscalaire */
772457
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Typiquement, la présence d'un cache de µops permet ce genre de fantaisies.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
g8qpysmpnd620668msg9qhw8bkta52s
772458
772457
2026-09-19T18:44:49Z
Mewtow
31375
/* Les unités d'émission/renommage d'un processeur superscalaire */
772458
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Typiquement, la présence d'un cache de µops permet ce genre de fantaisies.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K7, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
cgormozxw33a3kw598p1uo2st8jbv5y
772459
772458
2026-09-19T18:59:52Z
Mewtow
31375
/* L'émission multiple des accès mémoire */
772459
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double ! Typiquement, la présence d'un cache de µops permet ce genre de fantaisies.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
otw19nqxd79ebi87ozk3kbszjw8xf9q
772460
772459
2026-09-19T19:05:44Z
Mewtow
31375
/* Les unités d'émission/renommage d'un processeur superscalaire */
772460
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le séquenceur d'un processeur superscalaire étroit==
Le séquenceur d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
gamrcy74x0cn8902ptz0qzmegiuq6le
772472
772460
2026-09-19T20:00:09Z
Mewtow
31375
/* Le séquenceur d'un processeur superscalaire étroit */
772472
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le séquenceur d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''. Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
0lbwc6nctqajrfxtqz7on32vdgfwd7u
772473
772472
2026-09-19T20:00:35Z
Mewtow
31375
/* Le séquenceur d'un processeur superscalaire large */
772473
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative. Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
mvj1hoclsaduo83wyc854qyh92l0fri
772474
772473
2026-09-19T20:00:47Z
Mewtow
31375
/* Le front-end d'un processeur superscalaire large */
772474
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative. Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
9hkcdzxk3y8g16dhswlw3kyd2um8cpa
772475
772474
2026-09-19T20:00:56Z
Mewtow
31375
/* Le front-end d'un processeur superscalaire étroit */
772475
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative. Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel , un ''front-end'' étroit charge un bloc de 8, 16, 32 octets, qui contient plusieurs instructions, avec potentiellement des branchements dedans. Avec prédiction de branchement, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
j24v68wg9w0g5tlmxkoeo8b8rowko0k
772476
772475
2026-09-19T20:01:42Z
Mewtow
31375
/* L'étape de chargement d'un CPU superscalaire large */
772476
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative. Diverses solutions sont possibles pour amortir le problème. Elles permettent de distinguer les ''front-end étroits'' des ''front-end larges''.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un ''front-end étroit'', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un ''front-end large'', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
j6pdj7jl4gsoi4oxjo1lfen7gjpk67p
772477
772476
2026-09-19T20:02:02Z
Mewtow
31375
/* Le front-end d'un processeur superscalaire large */
772477
wikitext
text/x-wiki
Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Ils peuvent avoir plusieurs instructions qui s'exécutent en même temps, dans des unités de calcul séparées. C'est le cas dès qu'une instruction multi-cycles s'exécute. Par contre, ils ne peuvent démarrer qu'une seule instruction par cycle. Et quand on court après la performance, ce n'est pas assez ! Les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les '''processeurs à émissions multiples'''.
==Les différents types de CPU à émission multiple==
Un processeur à émission multiple charge plusieurs instructions en même temps, les décode en parallèle, puis les émet en même temps sur des unités de calculs séparées. Pour cela, il faut gérer les dépendances entre instructions, répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
[[File:Superscalarpipeline.svg|centre|vignette|upright=1.5|Pipeline RISC classique à cinq étages sur un processeur superscalaire. On voit bien que plusieurs instructions sont chargées en même temps.]]
===Les processeurs superscalaires et VLIW===
Les processeurs à émission multiple sont de deux types : les processeurs VLIW et les '''processeurs superscalaires'''. Nous mettons les processeurs VLIW de côté en attendant le prochain chapitre. La raison est qu'ils ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau du jeu d'instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre processeur VLIW et superscalaire est qu'un processeur superscalaire répartit les instructions sur les unités de calcul à l’exécution, là où un processeur VLIW délègue cette tâche au compilateur.
===Les processeurs superscalaires étroits et larges===
Sur un processeur à émission multiple, plusieurs micro-opérations sont émises en même temps, le nombre varie d'un processeur à l'autre. Les processeurs superscalaires les plus simples ne permettent que d'émettre deux micro-opérations à la fois, d'où leur nom de processeur ''dual issue'', ce qui se traduit en '''processeur à double émission'''. Les processeurs modernes peuvent émettre 3, 4, 5, 6, voire 8 micro-opérations simultanément. Aller au-delà de 10 instructions ne sert pas à grand-chose. En général, pour un processeur superscalaire capable d'émettre N instructions en parallèle, on parle de '''processeur superscalaire à N voies'''.
Il faut faire une distinction entre les processeurs superscalaires dits '''étroits''' et ceux dits '''larges'''. La différence tient dans le nombre de µops émises en même temps. Un processeur superscalaire étroit en émet peu en même temps, un large en émet beaucoup en même temps. Le nombre exact n'est pas fixé dans le marbre, la distinction est volontairement vague. Mais dans ce wikilivres, on part du principe que les processeurs superscalaires étroits émettent jusqu'à 4 µops à la fois, alors que les larges peuvent en émettre de 5 à 10. Et ce nombre ne sort pas de nulle part. Il est lié au fait que les branchements posent des problèmes sur les CPU superscalaires.
Les CPU superscalaires étroits utilisent une implémentation simple : ils chargent N instructions consécutives, les envoient dans N décodeurs, et exécutent les N µops correspondantes. Mais cette stratégie ne marche bien qu'en absence de branchements. La présence de branchements casse cette dynamique, et entraine une petite baisse de performance. Heureusement, pour un N petit, la perte de performance est mineure. Mais pour un N large, elle devient bien plus importante. La conception du CPU doit alors être repensée de fond en comble.
Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions, et un branchements pris toutes les 10 à 12 instructions. Ce qui que le CPU a tendance à charger des paquets de 4/5 instructions avant qu'un branchement viennent casser le flot d'exécution. Au-delà de 5 instructions, un CPU superscalaire doit être adapté pour gérer les branchements dans les paquets d'instruction qu'il émet. Le nombre de 5 instructions ne sort donc pas de nulle part.
==L'implémentation d'un processeur superscalaire==
Un processeur superscalaire peut utiliser l'exécution dans le désordre et le renommage de registres, mais ce n'est pas une obligation. Il existe des processeurs superscalaires à émission dans l'ordre, sans optimisations avancées. Le meilleur exemple est celui du Pentium 1 et 2 d'Intel. Ils étaient tous deux des processeurs superscalaires, mais n'avaient pas d'exécution dans le désordre, qui est apparue sur le Pentium 3. L'implémentation de la superscalarité est plus simple à expliquer avec émission dans l'ordre.
Prenons un processeur simple, sans exécution dans le désordre, et essayons d'ajouter la superscalarité. Le processeur de base est illustré ci-contre. Il est très simple : une unité de chargement, un décodeur, une unité de calcul, un banc de registres et une unité mémoire. Nous omettons volontairement l'unité d'émission, qui n'est pas représentée sur les schémas qui vont suivre.
[[File:Processeur non-superscalaire basique 01.png|centre|vignette|upright=1.5|Processeur non-superscalaire basique]]
Reste à adapter ce processeur pour qu'il soit capable de : lire plusieurs instructions depuis la mémoire, les décoder, les exécuter. Intuitivement, on se fit qu'on doit dupliquer tous les circuits : les décodeurs, l'unité de chargement, les unités de calcul, etc. Dans les faits, tout n'est pas dupliqué, certains circuits sont simplement adaptés.
===L'impact de la superscalarité sur le ''front-end''===
Un processeur superscalaire charge plusieurs instructions en même temps, ''en tenant compte de la présence de branchements''. La partie en italique est importante et elle a une conséquence très forte : la manière de faire est fortement différente entre un CPU superscalaire étroit et large.
Un CPU superscalaire étroit fait au plus simple : il charge un bloc de 2, 3, 4 instructions consécutives en mémoire RAM. Il faut alors doubler/tripler/quadrupler la taille du bus connecté au cache d'instruction. Par exemple, pour charger quatre instructions de 32 bits chacune, il suffit de lire un bloc de 128 bits dans le cache d'instruction. Il faut aussi tenir compte des branchements, mais cette prise en charge est minimale. Il faut juste repérer les branchements et ne pas charger les instructions à leur suite. Par contre, les CPU superscalaires larges ne peuvent pas se contenter de cela, et les solutions ne sont pas évidentes. Nous décrirons ces solutions dans la suite du chapitre, une section entière leur sera dédiée.
Pour décoder plusieurs instructions en même temps, le décodeur d'instruction est lui dupliqué. Le cout en transistor est important, mais c'est la solution la plus simple qui soit. Il y a des subtilités sur les processeurs microcodés, mais nous verrons cela dans une section dédiée. Pour le moment, retenez juste que les décodeurs sont dupliqués.
===L'impact de la superscalarité sur le chemin de données===
Pour ce qui est des unités de calcul, elles sont dupliquées. Pour le moment, on va considérer que les unités de calcul sont doublées avec la double émission, triplées avec la triple émission, et ainsi de suite. En réalité, la duplication est souvent partielle, certaines ALu ne sont pas dupliquées afin d'économiser des circuits, mais nous verrons cela plus tard. L'essentiel est que des unités de calcul sont ajoutées au processeur, comparé à ce qu'on aurait sans superscalarité. Et cela a des conséquences sur le reste du chemin de données.
Pour le banc de registre, il n'est pas dupliqué directement, mais on lui rajoute des ports de lecture et d'écriture. On rajoute des ports de lecture pour alimenter les unités de calcul rajoutées en opérandes, des ports d'écritures pour qu'elles puissent enregistrer leurs résultats dans les registres. Par exemple, prenons un processeur à double émission, pour lequel on aurait doublé le nombre d'unités de calcul. Qui dit deux fois plus d'ALUs dit : lire deux fois plus d'opérandes, écrire deux résultats. Les ports du banc de registre sont donc doublés.
[[File:Processeur non-superscalaire basique 03.png|centre|vignette|upright=2|Processeur non-superscalaire basique]]
Pour l'unité mémoire, il y a plusieurs solutions. Avec la plus simple, l'unité mémoire n'est pas dupliquée et le processeur superscalaire ne peut pas faire deux accès mémoire simultanés. Et ce n'est pas tant un problème que ça, du moins sur les processeurs à double ou triple émission. En effet, il est rare que deux instructions consécutives soient des instructions mémoire. Les situations où l'unité mémoire doit faire deux accès simultanés sont donc rares.
Notons que le processeur peut exécuter une instruction mémoire en parallèle d'une autre. Le cas le plus fréquent est une instruction mémoire suivie ou précédée d'une instruction de calcul. Et le processeur peut parfaitement exécuter la première dans l'unité mémoire et la seconde dans une ALU. Seuls les accès mémoire simultanés sont impossibles.
Par contre, dès qu'on passe à la quadruple émission ou au-delà, il est plus fréquent d'avoir des accès mémoire simultanés. Une autre solution double les ports de lecture/écriture du cache de données, ce qui permet plusieurs accès mémoire simultanés. L'unité mémoire est alors dupliquée, avec une unité mémoire par port du cache de données. Par exemple, un processeur double émission aura deux unités mémoire et un cache de données double port.
Par contre, si les processeurs superscalaires ajoutent des ports au cache, ils se limitent à deux ou trois ports, rarement plus. Par exemple, il est possible d'avoir un processeur quadruple émission, avec seulement deux ports sur le cache. Le processeur peut donc émettre deux accès mémoire à la fois, mais pas trois ni quatre. Et là encore, ce n'est pas un gros problème, car il est peu fréquent d'avoir trois ou quatre accès mémoire consécutifs.
[[File:Processeur non-superscalaire basique 02.png|centre|vignette|upright=2.5|Processeur non-superscalaire basique]]
Quelques processeurs utilisaient des solutions intermédiaires, un exemple classique étant les Pentium 1 et 2 d'Intel. Mais nous en parlerons dans le chapitre suivant.
===L'impact de la superscalarité sur l'unité d'émission===
Il ne nous reste plus qu'à voir l'unité d'émission, qu'on a volontairement passé sous silence dans ce qui précédait. L'unité d'émission existe toujours, mais elle est adaptée pour prendre en entrée deux micro-opérations, au lieu d'une seule. Elle détecte les dépendances avec les micro-opérations en vol, comme avant. Mais elle doit aussi détecter les dépendances entre micro-opérations en entrée, celles tout juste décodées. Le ''scoreboard'' est donc modifié pour détecter les dépendances entre les instructions à émettre, ce qui demande de rajouter des comparateurs.
Avec l'exécution dans le désordre, le ''scoreboard'' est remplacé par des fenêtres d'instruction, avec parfois une unité de renommage de registre en plus. Et ces circuits doivent être adaptés pour la superscalarité. Là encore, l'implémentation d'un processeur superscalaire demande de dupliquer plusieurs circuits et d'en adapter d'autres.
Pour ce qui est de des fenêtres d'instruction ou des stations de réservation, il faut pouvoir insérer et émettre plusieurs instructions à la fois. Pour cela, il suffit de rajouter des ports de lecture et écriture, et de modifier la logique de sélection en conséquence. Le ROB et les autres structures doivent aussi être modifiées pour pouvoir émettre et terminer plusieurs instructions en même temps, là encore en ajoutant des ports de lecture/écriture.
L'unité de renommage de registre n'est pas dupliquée, mais adaptée, pour gérer le cas où des instructions consécutives ont des dépendances de registre. Par exemple, prenons un processeur à double émission, qui renomme deux instructions consécutives. Si elles ont une dépendance, le renommage de la seconde instruction dépend du renommage de la première. La première doit être renommée et le résultat du renommage est utilisé pour renommer la seconde, ce qui empêche d'utiliser deux unités de renommage séparées.
===L’implémentation réelle n'est pas forcément celle décrite plus haut===
Pour résumer le tout, voici ce que cela donne dans les grandes lignes. Les décodeurs et les unités de calcul sont dupliqués, ce qui a un cout en circuit pas négligeable. Le banc de registre et l'unité mémoire deviennent multiport, le nombre de ports double pour de la double émission, triple ou de la triple émission, et ainsi de suite. L'unité de chargement charge deux fois plus de données, mais n'est pas modifiée en profondeur. Les circuits d'émission ou d'exécution dans le désordre sont un peu modifiés, les fene^tres d'instruction voient leur nombre de ports doubler/tripler/quadrupler.
{|class="wikitable"
|-
! rowspan="2" | Processeur sans émission multiple
| rowspan="2" | Chargement
| rowspan="2" | Décodage
| rowspan="2" | Renommage
| rowspan="2" | Émission
| Exécution / ALU
| rowspan="2" | ''Commit''/ROB
|-
| Exécution / ALU
|-
! rowspan="4" | Processeur superscalaire
| rowspan="4" | Chargement
| rowspan="2" | Décodage
| rowspan="4" | Renommage
| rowspan="4" | Émission
| Exécution / ALU
| rowspan="4" | ''Commit''/ROB
|-
| Exécution / ALU
|-
| rowspan="2" | Décodage
| Exécution / ALU
|-
| Exécution / ALU
|}
Le cout en circuit est assez important. Pour de la double émission, cela double approximativement le nombre de circuits utilisés. Je dis approximativement, car c'est moins que ça en réalité, vu que tout n'est pas dupliqué. Mais surtout, diverses optimisations permettent de réduire le cout en circuit, sans trop impacter les performances.
Nous avons parlé d'une de ces optimisations, plus haut, quand nous avons parlé de l'unité mémoire. Nous avions dit qu'il est possible de ne pas doubler/tripler les ports du cache, et de garder un cache simple port. Ou encore, d'augmenter le nombre de ports, mais pas au maximum permis par la triple/quadruple/octuple émission. Il s'agit là d'une économie qui réduit un peu les performances, mais avec un gain en matériel conséquent. Et bien sachez qu'il existe des optimisations similaires, pour les ports du banc de registre, pour l'unité d'émission, les décodeurs, et surtout : les unités de calcul.
Par exemple, j'ai dit plus haut que les unités de calcul étaient dupliquées. Concrètement, si le processeur de base a une ALU entière, un ''barrel shifter'', un circuit multiplieur et une FPU ; ils sont tous dupliqués. L'avantage de faire ainsi est que le processeur n'a pas de contrainte quand il veut émettre deux instructions. Si le processeur veut émettre deux multiplications consécutives, il le peut. S'il veut émettre deux instructions flottantes, il le peut. Pour le dire autrement, toutes les paires d'instructions possibles sont compatibles avec la double émission. Le problème, c'est que le cout en circuit est conséquent ! Dupliquer la FPU ou les circuits multiplieurs bouffe du transistor.
Pour économiser des transistors, il est possible de ne pas dupliquer toutes les ALU. Typiquement, les ALU entières simples sont dupliquées, mais pas la FPU, ni les circuits multiplieurs. Le cout en transistors est alors grandement réduit, au prix de l'apparition de dépendances structurelles. Par exemple, le CPU ne peut pas émettre deux multiplications consécutives sur un seul multiplieur. Ou encore, il ne peut pas émettre deux additions flottantes s'il y a un seul additionneur flottant.
La conséquence est que les processeurs superscalaires ont des '''contraintes d'appariement''' sur les instructions à émettre en même temps. Si on prend un processeur ''dual-issue'', il y a des paires d'instructions autorisées et des paires interdites. Par exemple, l'exécution simultanée de deux branchements est interdite, les branchements sont exécutés l'un après l'autre. Mais il est possible d'émettre un branchement en même temps qu'une autre instruction, en espérant que la prédiction de branchement ait fait une bonne prédiction. La raison est qu'il n'y a qu'une seule unité de calcul pour les branchements dans un processeur.
Pour résumer, tout n'est pas dupliqué à la perfection. Certaines structures matérielles ne le sont pas alors qu'elles le devraient, afin d'économiser du circuit. Dans la suite du chapitre, nous allons détailler ces optimisations. Et nous allons aussi détailler la conception de l'unité de chargement, de l'unité d'émission, des decodeurs superscalaires, et bien d'autres choses.
==Le ''front-end'' d'un processeur superscalaire étroit==
Pour rappel, le ''front-end'' est la partie du processeur qui charge les instructions et les décode. Elle regroupe les décodeurs, l'unité de prédiction de branchement, l'unité de ''Fetch'', le cache d'instruction. Le reste du processeur regroupe le chemin de données, l'unité d'émission, les files de µops et d'autres structures matérielles vues dans les chapitres précédents.
Le ''front-end'' d'un processeur superscalaire est modifié, afin de pouvoir charger et décoder plusieurs instructions à la fois. L'unité de chargement subit des modifications mineures, alors que les décodeurs sont simplement dupliqués. De plus, les unités de renommage et d'émission doivent être modifiées afin de pouvoir renommer et émettre plusieurs instructions à la fois. Voyons quelles sont les modifications en question.
===Le chargement de plusieurs instructions consécutives===
Pour charger plusieurs instructions, il suffit de doubler, tripler ou quadrupler le bus mémoire. Précisément, c'est le port de lecture du cache d’instruction qui est élargit, pour lire 2/3/4/... instructions. Un bloc de 8, 16, 32 octets est donc lu depuis le cache et est ensuite découpé en instructions, envoyées chacun à un décodeur. Reste à gérer les branchements.
Sans prédiction de branchement, toutes les instructions d'un bloc sont décodées et exécutées en même temps, même s'il y a un branchement dans le tas.
Avec prédiction de branchement, les instructions situées après le branchement sont annulées, elles ne sont pas décodées ni exécutées. L'unité de chargement coupe le bloc chargé au niveau du premier branchement non-pris, remplit les vides avec des NOP, avant d'envoyer le tout à l'unité de décodage. Les instructions à l'adresse de destination seront chargées au cycle suivant, en chargeant un nouveau bloc.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
===Le décodage parallèle des instructions===
Pour le décodage des instructions, plusieurs instructions sont décodées en même temps. On parle alors de '''décodage parallèle'''. Pour ce faire, un processeur superscalaire contient plusieurs décodeurs, chacun pouvant décoder une instruction en parallèle des autres. Prenons par exemple un processeur superscalaire à 4 voies, dont toutes les instructions font 32 bits. Un processeur superscalaire de ce type charge des blocs de 128 bits, ce qui permet de charger 4 instructions d'un seul coup. Et pour les décoder, le décodage se fera dans quatre décodeurs séparés, qui fonctionneront en parallèle. Cependant, cette implémentation ne marche bien que pour les processeurs RISC, les processeurs CISC devant faire avec un microcode gourmand en circuits.
Les processeurs CISC utilisent des décodeurs hybrides, avec un microcode qui complémente un décodeur câblé. Dupliquer le microcode aurait un cout en transistors trop important, ce qui fait que seuls les décodeurs câblés sont dupliqués. Les CPU CISC superscalaires disposent donc de plusieurs décodeurs simples, capables de décoder les instructions les plus courantes, avec un seul microcode. La conséquence est qu'il n'est pas possible de décoder deux instructions microcodées en même temps. Par contre, il reste possible de décoder plusieurs instructions non-microcodées ou une instruction microcodée couplée à une instruction non-microcodée. Vu qu'il est rare que deux instructions microcodées se suivent dans un programme, le cout en performance est extrêmement mineur.
===La macro-fusion : une optimisation du décodage parallèle===
Les processeurs superscalaires supportent la technique dite de '''macro-fusion''', qui permet de fusionner deux-trois instructions consécutives en une seule micro-opération. Par exemple, il est possible fusionner une instruction de test et une instruction de saut en une seule micro-opération de branchement. Il s'agit là de l'utilisation la plus importante de la macro-fusion sur les processeurs x86 modernes. La fusion des instructions se fait lors du décodage des instructions, grâce à la coopération des décodeurs. En théorie, d'autres utilisations de la macro-fusion sont possibles, certaines ont même été [https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-130.pdf proposées pour le jeu d'instruction RISC-V], mais n'ont pas été implémentées.
La macro-fusion est utilisée pour fusionner un test et un saut en un branchement unique. Une seconde utilisation, proposée sur le jeu d'instruction RISC-V, fusionnait un calcul d'adresse et une instruction LOAD/STORE en une seule instruction d'accès mémoire. Cette seconde utilisation a été envisagée à une époque où le jeu d'instruction RISC-V ne gérait pas les modes d'adressage base + indice. L'idée était d'avoir un pipeline mémoire qui gérait le mode d'adressage base + indice et de fusionner des instructions pour l'exploiter. Mais de nos jours, le jeu d'instruction RISC-V intégre le mode d'adressage base + indice, ce qui fait que la macro-fusion n'a aucun intérêt pour ce cas précis.
La macro-fusion a de nombreuses limitations en pratique. En théorie, on pourrait l'utiliser au-delà des deux situations mentionnées plus haut. Par exemple, pour fusionner une addition et une multiplication en une seule instruction MAD. Mais l'intérêt est assez faible. Notons que la macro-fusion ne permet que de fusionner des paires d'instructions. Aller au-delà demanderait un cout en circuit trop important. Déjà que tester deux paires utilise pas mal de comparateurs...
==Le ''front-end'' d'un processeur superscalaire large==
Décoder plusieurs instructions en parallèle pose de nombreux problèmes sur les processeurs superscalaires modernes, qui émettent de 5 à 10 instruction simultanées. Les analyses statistiques disent qu'il y a environ un branchement tous les 4 à 6 instructions. Un CPU a donc tendance à décoder des paquets de 3 à 5 instructions consécutives, avant qu'un branchement casse la dynamique. Or, cela pose des problèmes pour utiliser plusieurs décodeurs. L'implémentation basique, qui charge N instructions consécutives pour alimenter N décodeurs, fonctionne donc jusqu'à 4/6 décodeurs. Au-delà, on doit trouver une solution alternative.
La première solution utilise un cache de µops, avec un ''front-end'' capable de charger/décoder 4/5 instructions à la fois, pas plus. L'idée est que la majorité des instructions proviennent du cache de µops (plus de 80%), ce qui fait qu'on peut émettre environ 10 instructions à partir du cache de µops, pas besoin d'avoir beaucoup de décodeurs. Les performances sont cependant dégradées lors d'un défaut de cache de µops. Les instructions doivent alors être chargées depuis le cache d'instruction, par paquets de 4-5, puis sont décodées par 4-5 décodeurs, ce qui ne permet pas d’alimenter les unités de calcul à plein régime.
La solution précédente utilise un '''''front-end étroit''''', à savoir celui d'un CPU superscalaire étroit. Mais il est aussi possible d'utiliser un '''''front-end large''''', capable de charger une bonne dizaine d'instructions en même temps. La seule difficulté est de tenir compte de la présence de branchements.
===L'étape de chargement d'un CPU superscalaire large===
Pour rappel, avec un ''front-end'' étroit, les instructions situées après le premier branchement pris sont annulées, elles ne sont pas décodées ni exécutées.
[[File:Fetch sur un processeur superscalaire avec prediction de branchements.png|centre|vignette|upright=2|Fetch sur un processeur superscalaire avec prédiction de branchements.]]
Un ''front-end'' large charge les instructions de destination du branchement et les placent à sa suite. Ils chargent deux blocs à la fois et les fusionnent en un seul qui ne contient que les instructions présumées utiles.
[[File:Cache d'instructions autoaligné.png|centre|vignette|upright=2|Cache d'instructions autoaligné.]]
Mais cela demande de charger deux blocs de mémoire en une fois, ce qui demande un cache d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur (''merger''). En pratique, cette solution demande d'ajouter un second port de lecture au cache, pour un cout en hardware important, avec un impact négatif sur le temps d'accès au cache. Et il faut rajouter le fusionneur, qui prend un étage de pipeline à lui tout seul. Et encore, cette solution ne gère pas le cas où un bloc contient plusieurs branchements !
[[File:Implémentation d'un cache d'instructions autoaligné.png|centre|vignette|upright=2|Implémentation d'un cache d'instructions autoaligné.]]
Les exemples précédents partent du principe qu'il n'y a qu'un seul branchement dans un bloc. Mais quand les blocs deviennent assez long, il est fréquent d'avoir plusieurs branchements dans un bloc. Et il est même possible d'avoir plusieurs branchements pris. Dans un bloc de 8-10 instructions, ce n'est pas rare, et les CPU superscalaires à 8-10 voies sont la règle dans les CPU des années 2020. Pour gérer ce cas, il faut ajouter des ports supplémentaires au cache d'instruction et adapter le fusionneur, pour qu'il puisse fusionner trois ou quatre blocs. De plus, il faut utiliser une unité de prédiction de branchement capable de ''prédire plusieurs branchements par cycle''.
===Le décodage superscalaire large===
Le décodage des instructions n'est pas censé dépendre de l'implémentation du chargement. En théorie, une fois plusieurs blocs d'instruction fusionnés, on peut décoder les instructions en parallèle. Cependant, certains processeurs effectuent le décodage en tenant compte de la manière dont les branchements découpent les blocs. Les processeurs Atom d'Intel utilisent une solution de ce type, qui couple prédiction de branchement et décodage parallèle.
Ils chargent/décodent des paquets de 3 instructions consécutives, chaque paquet étant séparé du précédent par un branchement. Pour cela, en plus d'avoir un fusionneur adapté, ils regroupent les décodeurs en ''clusters'', chacun regroupant 3 décodeurs, et pouvant décoder trois instructions consécutives. Les microarchitectures Tremont et Crestmont avaient deux ''clusters'' (6 décodeurs au total), la microarchitecture Skymont en intègre trois (9 décodeurs au total).
Avec deux ''clusters'', le fonctionnement est le suivant. Si le code est composé d'une suite d'instruction sans branchements, seul le premier ''cluster'' est utilisé, limitant le décodage à trois instructions par cycle. Par contre, si l'une des trois instruction décodée est un branchement, alors le second ''cluster'' décode les trois instructions situés après ce branchement, à partir de l'adresse de destination. Avec un troisième ''cluster'', c'est la même chose, sauf qu'il faut qu'il y ait un branchement dans les trois instructions décodées par le second ''cluster''.
Mais comment faire pour savoir quels décodeurs activés, quels ''clusters'' sont actifs ? C'est la prédiction de branchement qui commande. Avec deux ''clusters'', elle détermine si un branchement est présent dans les trois instructions à charger, et quelle est son adresse de destination. Sans branchement, elle prévient les décodeurs et les trois instructions chargées sont décodées dans un ''cluster'' libre. Si elle prédit un branchement, elle charge les instructions adéquates, et active les deux ''clusters''. Avec trois branchements, c'est le même mécanisme, sauf que l'unité de prédiction de branchement doit être capable de prédire deux branchements par cycle.
Il faut noter que les décodeurs d'un ''cluster'' sont des décodeurs simples, sans microcode. Sur les processeurs Tremont et Crestmont, le microcode était unique et était partagé entre tous les ''clusters''. Un seul ''cluster'' pouvait décoder une instruction complexe avec le microcode, l'autre devait se limiter à des instructions simples uniquement. Depuis Skymont, le microcode est partiellement dupliqué, avec un microcode par ''cluster''.
Le cout en hardware peut paraitre prohibitif, pour un gain en performance faible. Sauf que c'est oublier que ce sont des processeurs Atom, sur lesquels beaucoup d'instructions sont microcodées. Les processeurs Atom ont un budget en transistor limité, ce qui fait qu'il est préférable pour eux d'implémenter des instructions en microcode que d'utiliser des unités de calcul dédiées ou des circuits de décodage complexes. Pour limiter la casse, le microcode n'est que partiellement dupliqué. Seul le microcode pour les instructions fréquente est dupliqué. Il y a un microcode secondaire pour certaines instructions, qui est lui unique et partagé par tous les ''clusters''.
==Les unités d'émission/renommage d'un processeur superscalaire==
Après avoir vu le ''front-end'', passons maintenant aux ''back-end''. Il contient le chemin de données, mais aussi les unités d'émission, de renommage, et de tout ce qui a trait à l'exécution dans l'ordre ou dans le désordre. La différence entre ''front-end'' et ''back-end'' est que le premier gère des instructions, alors que le second manipule des micro-opérations (µops). La distinction est importante, car une instruction correspond à une ou plusieurs µops. Il est donc possible de charger 5 instructions et de se retrouver avec 8 µops en sortie des décodeurs. Et c'est la source d'une distinction très importante.
Plus haut, j'ai parlé de processeurs superscalaires à N voies. La terminologie peut avoir deux interprétations. La première est que le processeur charge/décode N instructions à la fois. Une seconde est qu'il est capable d'exécuter/émettre N µops en même temps. Les deux ne sont pas équivalentes, vu qu'un processeur superscalaire peut, par exemple, charger/décoder N instructions et émettre/exécuter N+2 µops. La distinction est particulièrement importante sur les processeurs superscalaires larges. Certains d'entre eux sont capables de charger 5 instructions et d'en émettre le double !
Il y a cependant une limite à la quantité de µops qui peuvent être émises en même temps. L'étape limitante est souvent le renommage de registres. C'est en effet un des étape les plus séquentielle du pipeline. Pour renommer une µops, il faut généralement avoir renommé les précédentes. Du moins si les précédentes ont une dépendance de données avec la µops à renommer, mais c'est un détail.
Enfin, une dernière précision, cette fois-ci sur le ROB. Si un CPU superscalaire peut émettre 8 µops à la fois, alors on se dit que le ROB peut retirer 8 µops par cycle maximum. Et c'est très souvent le cas en pratique. Cependant, certains processeurs ont un ROB qui peut retirer le double ! Un exemple est celui des CPU de microarchitecture Skymont, qui peuvent émettre 8 µops par cycle, mais en retirer 16 par cycle. Quelques CPU de marque AMD étaient aussi dans ce cas. L'avantage est que cela permet de réduire la taille du ROB, du banc de registre et de quelques autres structures matérielles liées à l'exécution dans le désordre, le gain sur-compensant le cout en transistor nécessaire pour retirer 16 µops par cycle.
===L'unité de renommage superscalaire===
Sur un processeur à émission multiple, l'unité de renommage de registres doit renommer plusieurs instructions à la fois, mais aussi gérer les dépendances entre instructions. Pour cela, elle renomme les registres sans tenir compte des dépendances, pour ensuite corriger le résultat.
[[File:Unité de renommage superscalaire.png|centre|vignette|upright=2|Unité de renommage superscalaire.]]
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
[[File:Détection des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Détection des dépendances sur un processeur superscalaire.]]
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
[[File:Correction des dépendances sur un processeur superscalaire.png|centre|vignette|upright=2|Correction des dépendances sur un processeur superscalaire.]]
===L'unité d'émission superscalaire===
Pour émettre plusieurs instructions en même temps, l'unité d'émission doit être capable d'émettre plusieurs instructions par cycle. Et Pour cela, elle doit détecter les dépendances entre instructions. Il faut noter que la plupart des processeurs superscalaires utilisent le renommage de registre pour éliminer un maximum de dépendances inter-instructions. Les seules dépendances à détecter sont alors les dépendances RAW, qu'on peut détecter en comparant les registres de deux instructions consécutives.
Sur les processeurs superscalaires à exécution dans l’ordre, il faut aussi gérer l'alignement des instructions dans la fenêtre d'instruction. Dans le cas le plus simple, les instructions sont chargées par blocs et on doit attendre que toutes les instructions du bloc soient émises pour charger un nouveau bloc. Avec la seconde méthode, La fenêtre d'instruction fonctionne comme une fenêtre glissante, qui se déplace de plusieurs crans à chaque cycle d'horloge.
Mais au-delà de ça, le design de l'unité d'émission change. Avant, elle recevait une micro-opération sur son entrée, et fournissait une micro-opération émise sur une sortie. Et cela vaut aussi bien pour une unité d'émission simple, un ''scoreboard'', une fenêtre d'instruction ou des stations de réservation. Mais avec l'émission multiple, les sorties et entrées sont dupliquées. Pour la double émission, il y a deux entrées vu qu'elle doit recevoir deux micro-opération décodées/renommées, et deux sorties pour émettre deux micro-opérations. Formellement, il est possible de faire une analogie avec une mémoire : l'unité d'émission dispose de ports d'écriture et de lecture. On envoie des micro-opérations décodées/renommées sur des ports d'écriture, et elle renvoie des micro-opérations émises sur le port de lecture. Dans ce qui suit, nous parlerons de '''ports de décodage''' et de '''ports d'émission'''.
Intuitivement, on se dit qu'il y a autant de ports de décodage que de ports d'émission. Mais la présence d'un cache de µops découple le nombre d'instructions décodées par cycle avec le nombre de µops émises. Par exemple, le cache de µops peut alimenter une dizaine d'unités de calcul, alors que le processeur a seulement 5 décodeurs. Il y a ainsi une différence entre nombre d'instructions décodées par cycle d'horloge et nombre de µops émises par l'unité d'émission. Par contre, ce rythme idéal n'a lieu qu'en cas de succès dans le cache de µops. Le moindre défaut de cache entraine l'usage des décodeurs, pour alimenter les unités de calcul.
Si l'unité d'émission est un vulgaire ''scoreboard'', il doit détecter les dépendances entre instructions émises simultanément, il doit détecter les paires d'instructions dépendantes et les sérialiser. Par contre, la détection des dépendances entre instructions consécutives est simplifiée avec une fenêtre d'instruction, il n'y a pour ainsi dire pas grand chose à faire, vu que les dépendances sont éliminées par le renommage de registre et que les signaux de réveil s'occupent de gérer les dépendances RAW. C'est la raison pour laquelle les processeurs superscalaires utilisent tous une fenêtre d'instruction centralisée ou décentralisée, et non des ''scoreboard''.
===Les fenêtres d'instruction et stations de réservation des CPU superscalaires===
Les processeurs superscalaires privilégient souvent des stations de réservations aux fenêtres d'instruction. Rappelons la terminologie utilisée dans ce cours. Les fenêtres d'instruction se contentent de mémoriser la micro-opération à émettre et quelques bits pour la disponibilité des opérandes. Par contre, les stations de réservations mémorisent aussi les opérandes des instructions. Les registres sont lus après émission avec une fenêtre d'instruction, avant avec des stations de réservation. Et cette différence a une influence sur le banc de registre, et précisément au nombre de ports de lecture.
Supposons que toutes les instructions sont dyadiques, ou du moins qu'il n'existe pas de micro-opération à trois opérandes. Avec une fenêtre d'instruction, le nombre d'opérandes à lire simultanément est le double de la quantité d'instructions qu'on peut émettre en même temps. Sur un processeur ''dual issue'', qui peut émettre deux micro-opérations, cela fait deux micro-opérations à deux opérandes chacune, soit 4 opérandes. Le nombre de ports de lecture est donc de quatre. Avec une station de réservation, la lecture des opérandes a lieu avant l'émission, juste après le décodage/renommage. Le nombre d'opérande est le double du nombre de micro-opérations décodées/renommées, pas émises. Si le décodeur peut décoder/renommer 4 instructions par cycle, cela veut dire 4 micro-opérations émises par cycle.
Et il se trouve que les deux situations ne sont pas évidentes. Avec une fenêtre d'instruction centralisée, le nombre d'instructions décodées et émises en même temps sont identiques. Mais dès qu'on utilise des fenêtres d'instruction décentralisées, les choses changent. Si le décodeur peut décoder/renommer 4 instructions par cycle, alors l'idéal est d'avoir 4 micro-opérations émises par cycle, ''par fenêtre d'instruction''. Imaginez que le décodeur décode 4 instructions entières : la fenêtre d'instruction entière doit pouvoir émettre 4 micro-opérations entières en même temps. Idem pour des instructions flottantes avec la fenêtre d'instruction flottantes. Vu qu'il y a deux fenêtres d'instruction, cela fait 4 micro-opérations entières + 4 micro-opérations flottantes = 8 ports de lecture. Non pas qu'ils soient tous simultanément utiles, mais il faut les câbler.
===Les conséquences sur le banc de registre===
Émettre plusieurs instructions en même temps signifie lire plusieurs opérandes à la fois : le nombre de ports du banc de registres doit être augmenté. De plus, il faut aussi écrire plusieurs résultats à la fois dans les registres, ce qui rajoute des ports d'écriture. Il faut au maximum 2 ports de lecture et un port d'écriture par unité de calcul. Le problème, c'est que plus un banc de registres a de ports, plus il utilise de circuits, est compliqué à concevoir, consomme de courant et chauffe. Et avec plusieurs dizaines d'unités de calcul différentes, le câblage est tout simplement ignoble. Mais diverses optimisations permettent de réduire le nombre de ports assez simplement.
Un autre solution utilise un banc de registre unique, mais n'utilise pas autant de ports que le pire des cas le demanderait. Pour cela, le processeur doit détecter quand il n'y a pas assez de ports pour servir toutes les instructions. L'unité d'émission devra alors mettre en attente certaines instructions, le temps que les ports se libèrent. Cette détection est réalisée par un circuit d'arbitrage spécialisé, intégré à l'unité d'émission, l’'''arbitre du banc de registres''' (''register file arbiter'').
Un autre détail est que le nombre de ports de lecture n'est pas le même selon que le processeur utilisé des stations de réservation ou une fenêtre d’instruction. Les fenêtres d'instruction impliquent plus de lectures d'opérandes, ce qui implique plus de ports de lecture sur le banc de registres. Les stations de réservation sont plus économes, elles vont bien avec un nombre modéré de ports de lecture.
==Les unités de calcul des processeurs superscalaires==
Un processeur superscalaire émet/exécute plusieurs instructions simultanément dans plusieurs unités de calcul séparées. Intuitivement, on se dit qu'il faut dupliquer les unités de calcul à l'identique. Un processeur superscalaire contient alors N unités de calcul identiques, ce qui permet d'émettre N micro-opérations, tant qu'elles n'ont pas de dépendances. Manque de chance, ce cas est l'exception, pas la règle. Dupliquer toutes les ALU aurait un cout en circuit bien trop important. À la place, la duplication des unités de calcul n'est que partielle. Certaines ALU sont dupliquées, d'autres ne le sont pas.
Il est même possible d'avoir un processeur superscalaire sans avoir à dupliquer la moindre ALU ! Il faut dire que les processeurs superscalaires usuels ont un pipeline dynamique, à savoir qu'ils incorporent plusieurs unités de calcul distinctes. Typiquement, une ALU pour les instructions entières, une FPU pour les instructions flottantes, une unité pour les accès mémoire (calcul d'adresse) et une unité pour les tests/branchements. Sans superscalarité, ces unités sont toutes reliées au même port d'émission. Avec superscalarité, ces unités de calcul existantes sont connectées à des ports d'émission différents. Voyons cela en détail.
===La double émission entière-flottante===
Prenons le cas d'un processeur avec une ALU entière et une FPU flottante, qu'on veut transformer en processeur superscalaire. La solution la plus simple est d'émettre une micro-opération entière en même temps qu'une micro-opération flottante. La micro-opération entière s'exécute dans l'ALU entière, la micro-opération flottante dans la FPU. Il suffit juste d'ajouter un port d'émission dédié sur la FPU. Le processeur a donc deux pipelines séparés : un pour les micro-opérations entières, un autre pour les micro-opérations flottantes. On parle alors de '''double émission entière-flottante'''.
La mise en œuvre utilise assez peu de circuits, du moins sur les processeurs sans exécution dans le désordre. Concrétement, il suffit de scinder le décodeur en deux décodeurs séparés, de charger deux instructions à la fois, et d'ajouter quelques circuits pour la double émission. Le gain en performance qui est faible, mais en vaut la peine.
La plupart des premiers processeurs superscalaires étaient de ce type. Les exemples les plus notables sont les processeurs POWER 1 et ses dérivés comme le ''RISC Single Chip''. Ils sont assez anciens et avaient un budget en transistors limité, ce qui fait qu'ils devaient se débrouiller avec peu de circuits dont ils disposaient. L'usage d'une double émission entière-flottante était assez naturelle.
[[File:Double émission entière-flottante.png|centre|vignette|upright=2|Double émission entière-flottante]]
===L'émission multiple des micro-opérations flottantes===
La double émission entière-flottante ajoute un port d'émission pour la FPU, ce qui a un cout en circuits modeste, pour un gain en performance intéressant. Mais il faut savoir que les FPU regroupent un additionneur-soustracteur flottant et un multiplieur flottant, qui sont reliés au même port d'émission. Et il est possible d'ajouter des ports d'émission séparés pour l'additionneur flottant et le multiplieur flottant. Le processeur peut alors émettre une addition flottante en même temps qu'une multiplication flottante. Les autres circuits de calcul flottant sont répartis sur ces deux ports d'émission flottants.
L'avantage est que cela se marie bien avec l'usage d'une fenêtre d'instruction séparée pour les opérations flottantes. La fenêtre d'instruction a alors deux ports séparés, au lieu d'un seul. Rajouter un second port d'émission flottant n'est pas trop un problème, car le cout lié à l'ajout d'un port n'est pas linéaire. Passer de un port à deux a un cout tolérable, bien plus que de passer de 3 ports à 4 ou de 4 à 5.
Il est même possible de dupliquer l'additionneur et le multiplieur flottant, ce qui permet d'émettre deux additions et multiplications flottantes en même temps. C'est ce qui est fait sur les processeur AMD de architecture Zen 1 et 2. Ils ont deux additionneurs flottants par cœur, deux multiplieurs flottants, chacun avec leur propre port d'émission. Les performances en calcul flottant sont assez impressionnantes pour un processeur de l'époque.
[[File:ZEN - émission multiple flottante.png|centre|vignette|upright=2|Microarchitecture Zen 1 d'AMD.]]
===L'émission multiple des micro-opérations entières===
Nous avons vu plus haut qu'il est possible d'ajouter plusieurs ports d'émission pour la FPU. Intuitivement, on se dit que la méthode peut aussi être appliquée pour les ALU entières. En effet, un processeur contient généralement plusieurs unités de calcul entières séparées, avec typiquement une ALU entière simple, un circuit multiplieur/diviseur, un ''barrel shifter'', parfois une unité de manipulation de bit. La présence de ces unités permet d'émettre jusqu'à 4 micro-opérations entières en même temps, à condition qu'on ajoute assez de ports d'émission.
[[File:Emission multiple des opérations entières, implémentation naive.png|centre|vignette|upright=2|Émission multiple des opérations entières, implémentation naïve.]]
Maintenant, posons-nous la question : est-ce que faire ainsi en vaut la peine ? Le processeur peut en théorie émettre une addition, une multiplication, un décalage et une opération de manipulation de bits en même temps. Mais une telle situation est rare, ce qui fait que les ports d'émission seront sous-utilisés. Pour réduire le nombre de ports d'émission sous-utilisés, il est possible de regrouper plusieurs unités de calcul sur le même port d'émission.
Pour décider comment faire le regroupement, il faut se baser sur la fréquence des instructions. Les instructions arithmétiques sont plus fréquentes que les décalages et les opérations de manipulation de bit. Et elles sont souvent appariées, à savoir qu'une multiplication suit ou précède souvent une autre opération arithmétique, et vis versa. En conséquence, il est préférable d'avoir un port d'émission pour l'ALU entière, un autre pour le multiplieur. L'avantage est que cela permet d'exécuter des micro-opérations entières en parallèle d'une multiplication. Les multiplications étant des instructions à la fois multicycles et assez fréquentes, une telle situation n'est pas rare. Les unités restantes sont placées sur l'un des deux ports.
[[File:Emission multiple des opérations entières, double émission.png|centre|vignette|upright=1.5|Émission multiple des opérations entières, double émission]]
Typiquement, la plupart des programmes sont majoritairement remplis d'additions, avec des multiplications assez rares et des décalages qui le sont encore plus. En pratique, il n'est pas rare d'avoir une multiplication pour 4/5 additions. Si on veut profiter au maximum de l'émission multiple, il faut pouvoir émettre plusieurs additions/soustractions en même temps, ce qui demande de dupliquer les ALU simples et leur donner chacune son propre port d'émission. Le multiplieur n'est presque jamais dupliqué, car il est rare d'avoir plusieurs multiplications consécutives. Disposer de plusieurs circuits multiplieurs serait donc un cout en circuits qui ne servirait que rarement et n'en vaut pas la chandelle.
Pour économiser des ports d'émission, les ALU entières dupliquées sont reliées à des ports d'émission existants. Par exemple, on peut ajouter la seconde ALU entière au port d'émission du multiplieur, la troisième ALU entière au port dédié au ''barrel shifter'', etc. Ainsi, les ports d'émission sont mieux utilisés : il est rare qu'on n'ait pas d'instruction à émettre sur un port. Le résultat est un gain en performance bien plus important qu'avec les techniques précédentes, pour un cout en transistor mineur.
[[File:Emission multiple des opérations entières, implémentation courante.png|centre|vignette|upright=1.5|Emission multiple des opérations entières, implémentation courante]]
===L'émission multiple des accès mémoire===
Plus haut, nous avons parlé de la double émission entière-flottante. Et bien sachez qu'une optimisation similaire peut être appliquée aux accès mémoire. En théorie, les accès mémoire sont pris en charge par le pipeline pour les opérations entières, ce qui permet d'utiliser l'unité de calcul pour calculer des adresses. Mais il est aussi possible de relier l'unité mémoire à son propre port d'émission. Le processeur devient alors capable d’émettre une micro-opération mémoire en parallèle d'autres micro-opération entières/flottantes. On parle alors de '''triple émission entière-flottante-mémoire'''. La seule contrainte est que l'unité mémoire incorpore une unité de calcul d'adresse dédiée.
Et comme pour pour les opérations flottantes et entières, il est possible d'avoir une '''émission multiple des accès mémoire''' ! Il est en effet possible d'émettre plusieurs micro-opérations mémoire en même temps. Les processeurs superscalaires modernes sont capables d'émettre plusieurs lectures/écritures simultanément. Par exemple, ils peuvent émettre une lecture en même temps qu'une écriture, ou plusieurs lectures, ou plusieurs écritures. Pour cela, il faut idéalement que le cache soit multiport, afin de pouvoir servir plusieurs accès mémoire en même temps.
Il faut noter que selon le processeur, il peut y avoir des restrictions quant aux accès mémoire émis en même temps. Par exemple, certains processeurs peuvent émettre une lecture avec une écriture en même temps, mais pas deux lectures ni deux écritures. Ou encore, ils peuvent émettre deux lectures, une lecture et une écriture, mais pas deux écritures en même temps. Dans la majorité des cas, les processeurs ne permettent pas d'émettre deux écritures en même temps, alors qu'ils supportent plusieurs lectures. Il faut dire que les lectures sont plus fréquentes que les écritures. Les processeurs qui autorisent toutes les combinaisons de lecture/écriture possibles, sont rares.
L'émission multiple des accès mémoire demande évidemment de dupliquer des circuits, mais pas l'unité mémoire complète. Pour rappel, l'unité mémoire s'interpose entre le cache et le reste du pipeline. Elle incorpore le cache de données L1, une file d'écriture, potentiellement des unités de calcul. Mais sur les processeurs superscalaires modernes, les unités de calcul sont placées en-dehors de l'unité mémoire. Pour rappel, l'unité mémoire est précédée par une structure qui met en attente les micro-opérations mémoire avant leur émission finale. La structure en question varie suivant l'implémentation, mais il y a trois cas principaux qu'on va voir dans l'ordre.
Le cas le plus simple est celui où l'unité mémoire est précédée par une file de µops mémoire unique, mais rares sont les processeurs à émission multiple qui sont dans ce cas. Les rares processeurs commerciaux à faire ainsi sont les processeurs AMD de micro-architecture K7 K8, à savoir les anciens AMD Athlon et AMD Athlon 64. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. Par contre, la file de µops mémoire pouvait recevoir trois adresses par cycle, calculées par trois unités de calcul d'adresse distinctes.
: La file de µops mémoire est appelée la ''Pre-Cache Queue'' dans les schéma qui suivent. La ''Post-Cache Queue'' est une structure servant à gérer les défauts de cache, elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Sur la microarchitecture K7, les unités de calcul d'adresse sont alimentées par une fenêtre d'instruction qui gère à la fois les opérations entières et les calculs d'adresse. La triple émission était donc hybride : trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
[[File:AMD K7.png|centre|vignette|upright=2|AMD K7]]
Sur la microarchitecture K8, il y a plusieurs fenêtre d'instruction, chacune gérant à la fois une ALU entière et une AGU de calcul d'adresse. La triple émission ne changeait pas, on avait toujours trois calculs d'adresse par cycle, mais un accès mémoire à la fois.
[[File:AMD K8, microarchitecture.png|centre|vignette|upright=2|AMD K8]]
Le cas avec deux files de micro-opération permet d'implémenter la double émission très simplement. Pour rappel, il s'agit du cas avec une file pour les lectures et une autre pour les écritures, appelées respectivement la file de µops LOAD et la file de µops STORE. Dans ce cas, il est possible d'émettre une lecture et une écriture lors du même cycle d'horloge. Il faut dire que l'implémentation est facilitée par le fait que l'unité mémoire est naturellement double port, avec un port de lecture et un port d'écriture. Le port de lecture alimente une unité de calcul d'adresse dédiée, directement reliée au cache. Le port d'écriture du cache alimente une unité de calcul, qui est suivie par une file d'écriture, elle-même reliée au cache. Les processeurs AMD de micro-architecture K6 utilisaient cette technique, bien qu'ils étaient antérieurs aux micro-architectures K7/K8 vues précédemment.
[[File:Double émission avec le Load Ordering, Store Ordering.jpg|centre|vignette|upright=2.5|Double émission avec le Load Ordering, Store Ordering]]
Il est possible d'adapter le tout pour la triple ou quadruple émission. Ajouter la possibilité d'émettre une seconde écriture est assez compliqué et demande d'utiliser une file d'écriture multiport, implémenter des lectures en plus est plus simple. Il faut pour cela rajouter un port de lecture au cache, à l'unité mémoire, et une file de µops LOAD. Les deux files de µops LOAD sont alimentées via un port d'émission chacune.
Les processeurs avec une ''Load-store queue'' ne la dupliquent pas, mais la rende multi-ports afin de gérer plusieurs micro-opérations mémoire simultanées. Par exemple, les processeurs skylake ont une LSQ avec deux ports de lecture et un port d'écriture, ce qui permet de faire deux lectures en même temps qu'une écriture. Il faut cependant dupliquer les unités de calcul d'adresse reliées à la LSQ, chaque port d'émission mémoire ayant sa propre unité de calcul d'adresse.
: Dans ce qui suit, nous parlerons d'AGU (''Adress Generation Unit'') pour désigner les unités de calcul d'adresse.
[[File:Emissim multiple des µops mémoire.png|centre|vignette|upright=2.5|Emission multiple des µops mémoire.]]
Un exemple est celui des processeurs Intel de microarchitecture Nehalem, qui pouvaient seulement émettre une lecture en même temps qu'une écriture. Ils avaient deux ports d'émission reliés à l'unité mémoire. Un port pour les lectures, un autre pour les écritures. Le premier port d'écriture recevait la donnée à écrire et s'occupait des calculs d'adresse, Le port de lecture faisait uniquement des calculs d'adresse.
D'autres processeurs ont plusieurs ports d'émission pour les unités mémoire, mais qui peuvent faire indifféremment lecture comme écritures. Un exemple est celui du processeur Athlon 64, un processeur AMD sorti dans les années 2000. Il disposait d'une LSQ unique, reliée à un cache L1 de donnée double port. La LSQ était reliée à trois unités de calcul séparées de la LSQ. La LSQ avait des connexions avec les registres, pour gérer les lectures/écritures.
Il faut noter que sur certains CPU, il peut y avoir les ports pour les écritures peuvent permettre plus de calculs d'adresse que d'écritures proprement dites. Par exemple, la microarchitecture Skymont d'Intel dispose de 4 unités de calcul d'adresse, mais de deux seulement deux ports pour les données à écrire. La raison est que cela permet de calculer les adresses des écritures en avance, ce qui permet à la désambiguïsation mémoire de faire un meilleur travail.
===L'interaction avec les fenêtres d'instruction===
Nous venons de voir qu'un processeur superscalaire peut avoir des ports d'émission reliés à plusieurs ALU. Pour le moment, nous avons vu le cas où le processeur dispose de fenêtres d'instruction séparées pour les opérations entières et flottantes. Un port d'émission est donc relié soit à des ALU entières, soit à des FPU. Mais il existe des processeurs où un même port d'émission alimente à la fois une ALU entière et une FPU. Par exemple, on peut relier un additionneur flottant sur le même port qu'une ALU entière. Il faut noter que cela implique une fenêtre d'instruction centralisée, capable de mettre en attente micro-opérations entières et flottantes.
Un exemple est celui des processeurs Core 2 Duo. Ils disposent de 6 ports d'émission, dont 3 ports dédiés à l'unité mémoire. Les 3 ports restants alimentent chacun une ALU entière, un circuit de calcul flottant et une unité de calcul SSE (une unité de calcul SIMD, qu'on abordera dans quelques chapitres).
* Le premier port alimente une ALU entière simple et un multiplieur/diviseur flottant.
* Le second alimente une ALU entière, un multiplieur entier et un additionneur flottant.
* Le troisième alimente une ALU entière, sans circuit flottant dédié.
Une conséquence de partager les ports d'émission est l'apparition de dépendances structurelles. Par exemple, imaginez qu'on connecte un multiplieur entier et la FPU, sur le même port d'émission. Il est alors impossible d'émettre une multiplication et une opération flottante en même temps. Mais la situation ne se présente que pour certaines combinaisons de micro-opérations bien précises, qui sont idéalement assez rares. De telles dépendances structurelles n'apparaissent que sur des programmes qui entremêlent instructions flottantes et entières, ce qui est assez rare. Les dépendances structurelles doivent cependant être prises en compte par les unités d'émission.
Dans le même genre, il est possible de partager un port d'émission entre l'unité mémoire et une ALU entière. Cela permet d'utiliser l'ALU entière pour les calculs d'adresse, ce qui évite d'avoir à utiliser une unité de calcul d'adresse distincte. Un exemple est celui du processeur superscalaire double émission Power PC 440. Il dispose de deux ports d'émission. Le premier est connecté à une ALU entière et un circuit multiplieur, le second est relié à l'unité mémoire et une seconde ALU entière. L'organisation en question permet soit d'émettre un accès mémoire en même temps qu'une opération entière, soit d'émettre deux opérations entières simples, soit d’émettre une multiplication et une addition/soustraction/comparaison. Une organisation simple, mais très efficace !
[[File:PowerPC 440.png|centre|vignette|upright=2|Microarchitecture du PowerPC 440.]]
===Résumé===
Faisons un résumé rapide de cette section. Nous venons de voir que les différentes unités de calcul sont reliés à des ports d'émission, la répartition des ALU sur les ports d'émission étant très variable d'un processeur à l'autre. Entre les processeurs qui séparent les ports d'émission entier et flottant, ceux qui les mélangent, ceux qui séparent les ports d'émission mémoire des ports entiers et ceux qui les fusionnent, ceux qui autorisent l'émission multiple des micro-opérations mémoire ou flottante, il y a beaucoup de choix.
Les divers choix possibles sont tous des compromis entre deux forces : réduire le nombre de ports d'émission d'un côté, garder de bonnes performances en limitant les dépendances structurelles de l'autre. Réduire le nombre de ports d'émission permet de garder des fenêtres d'instruction relativement simples. Plus elles ont de ports, plus elles consomment d'énergie, chauffent, sont lentes, et j'en passe. De plus, plus on émet de micro-opérations en même temps, plus la logique de détection des dépendances bouffe du circuit. Et cela a des conséquences sur la fréquence du processeur : à quoi bon augmenter le nombre de ports d'émission si c'est pour que ce soit compensé par une fréquence plus faible ?
Par contre, regrouper plusieurs ALU sur un même port d'émission est à l'origine de dépendances structurelles. Impossible d'émettre deux micro-opérations sur deux ALU si elles sont sur le même port. Le nombre de ports peut être un facteur limitant pour la performance dans certaines situations de '''''port contention''''' où on a assez d'ALU pour exécuter N micro-opérations, mais où la répartition des ALUs sur les ports l’empêche. Suivant la répartition des ALU sur les ports, la perte de performance peut être légère ou importante, tout dépend des choix réalisés. Et les choix en question dépendent fortement de la répartition des instructions dans le programme exécuté. Le fait que certaines instructions sont plus fréquentes que d'autres, que certaines instructions sont rarement consécutives : tout cela guide ce choix de répartition des ALu sur les ports.
==Le contournement sur les processeurs superscalaires==
Pour rappel, la technique du contournement (''register bypass'') permet au résultat d'une instruction d'être immédiatement utilisable en sortie de l'ALU, avant même d'être enregistré dans les registres. Implémenter la technique du contournement demande d'utiliser des multiplexeurs pour relier la sortie de l'unité de calcul sur son entrée si besoin. il faut aussi des comparateurs pour détecter des dépendances de données.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
===Les problèmes du contournement sur les CPU avec beaucoup d'ALUs===
Avec plusieurs unités de calcul, la sortie de chaque ALU doit être reliée aux entrées de toutes les autres, avec les comparateurs qui vont avec ! Sur les processeurs ayant plusieurs d'unités de calculs, cela demande beaucoup de circuits. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul.
De plus, la complexité du réseau de contournement (l'ensemble des interconnexions entre ALU) a un cout en terme de rapidité du processeur. Plus il est complexe, plus les données contournées traversent de longs fils, plus leur temps de trajet est long, plus la fréquence du processeur en prend un coup. Diverses techniques permettent de limiter la casse, comme l'usage d'un bus de contournement, mais elle est assez impraticable avec beaucoup d'unités de calcul.
Notez que cela vaut pour les processeurs superscalaires, mais aussi pour tout processeur avec beaucoup d'unités de calcul. Un simple CPU à exécution dans le désordre, non-superscalaire, a souvent pas mal d'unités de calcul et fait face au même problème. En théorie, un processeur sans exécution dans le désordre ou superscalarité pourrait avoir le problème. Mais en pratique, avoir une dizaine d'ALU implique processeur superscalaire à exécution dans le désordre. D'où le fait qu'on parle du problème maintenant.
La seule solution praticable est de ne pas relier toutes les unités de calcul ensemble. À la place, on préfère regrouper les unités de calcul dans différents '''agglomérats''' ('''cluster'''). Le contournement est alors possible entre les unités d'un même agglomérat, mais pas entre agglomérats différents. Généralement, cela arrive pour les unités de calcul entières, mais pas pour les unités flottantes. La raison est que les CPU ont souvent beaucoup d'unités de calcul entières, car les instructions entières sont légion, alors que les instructions flottantes sont plus rares et demandent au mieux une FPU simple.
Évidemment, l'usage d'agglomérats fait que certaines possibilités de contournement sont perdues, avec la perte de performance qui va avec. Mais la perte en possibilités de contournement vaut bien le gain en fréquence et le cout en circuit/fils réduit. C'est un bon compromis, ce qui explique que presque tous les processeurs modernes l'utilisent. Les rares exceptions sont les processeurs POWER 4 et POWER 5, qui ont préféré se passer de contournement pour garder un processeur très simple et une fréquence élevée.
===Les bancs de registre sont aussi adaptés pour le contournement===
L'usage d'agglomérats peut aussi prendre en compte les interconnexions entre unités de calcul et registres. C'est-à-dire que les registres peuvent être agglomérés. Et cela peut se faire de plusieurs façons différentes.
Une première solution, déjà vue dans les chapitres sur la micro-architecture d'un processeur, consiste à découper le banc de registres en plusieurs bancs de registres plus petits. Il faut juste prévoir un réseau d'interconnexions pour échanger des données entre bancs de registres. Dans la plupart des cas, cette séparation est invisible du point de vue de l'assembleur et du langage machine. Le processeur se charge de transférer les données entre bancs de registres suivant les besoins. Sur d'autres processeurs, les transferts de données se font via une instruction spéciale, souvent appelée ''COPY''.
[[File:Banc de registres distribué.png|centre|vignette|upright=2|Banc de registres distribué.]]
Sur de certains processeurs, un branchement est exécuté par une unité de calcul spécialisée. Or les registres à lire pour déterminer l'adresse de destination du branchement ne sont pas forcément dans le même agglomérat que cette unité de calcul. Pour éviter cela, certains processeurs disposent d'une unité de calcul des branchements dans chaque agglomérat. Dans les cas où plusieurs unités veulent modifier le ''program counter'' en même temps, un système de contrôle général décide quelle unité a la priorité sur les autres. Mais d'autres processeurs fonctionnent autrement : seul un agglomérat possède une unité de branchement, qui peut recevoir des résultats de tests de toutes les autres unités de calcul, quel que soit l’agglomérat.
Une autre solution duplique le banc de registres en plusieurs exemplaires qui contiennent exactement les mêmes données, mais avec moins de ports de lecture/écriture. Un exemple est celui des processeurs POWER, Alpha 21264 et Alpha 21464. Sur ces processeurs, le banc de registre est dupliqué en plusieurs exemplaires, qui contiennent exactement les mêmes données. Les lectures en RAM et les résultats des opérations sont envoyées à tous les bancs de registres, afin de garantir que leur contenu est identique. Le banc de registre est dupliqué en autant d'exemplaires qu'il y a d'agglomérats. Chaque exemplaire a exactement deux ports de lecture, une par opérande, au lieu de plusieurs dizaines. La conception du processeur est simplifiée, que ce soit au niveau du câblage, que de la conception des bancs de registres.
==Les optimisations de la pile d'appel : le ''stack engine''==
Les processeurs modernes intègrent une optimisation liée au pointeur de pile. Pour rappel, sur les architectures modernes, le pointeur de pile est un registre utilisé pour gérer la pile d'appel, précisément pour savoir où se trouve le sommet de la pile. Il est manipulé par les instructions CALL, RET, PUSH et POP, mais aussi par les instructions d'addition/soustraction pour gérer des cadres de pile de taille variable. De plus, il peut servir d'opérande pour des instructions LOAD/STORE, afin de lire/écrire des variables locales, les arguments d'une fonction, et autres. C'est donc un registre général adressable, intégré au banc de registre, altéré par l'unité de calcul entière.
L'incrémentation/décrémentation du pointeur de pile passe donc par l'unité de calcul, lors des instructions CALL, RET, PUSH et POP. Mais, l'optimisation que nous allons voir permet d'incrémenter/décrémenter le pointeur de pile sans passer par l'ALU, ou presque. L'idée est de s'inspirer des architectures avec une pile d'adresse de retour, qui intègrent le pointeur de pile dans le séquenceur et l'incrémentent avec un incrémenteur dédié.
===Le ''stack engine''===
L'optimisation que nous allons voir utilise un '''''stack engine''''' intégré à l'unité de contrôle, au séquenceur. Le processeur contient toujours un pointeur de pile dans le banc de registre, cela ne change pas. Par contre, il n'est pas incrémenté/décrémenté à chaque instruction CALL, RET, PUSH, POP. Un compteur intégré au séquenceur est incrémenté à la place, nous l’appellerons le '''compteur delta'''. La vraie valeur du pointeur de pile s'obtient en additionnant le compteur delta avec le registre dans le banc de registre. Précisons que si le compteur delta vaut zéro, la vraie valeur est dans le banc de registre et peut s'utiliser telle quelle.
Lorsqu'une instruction ADD/SUB/LOAD/STORE utilise le pointeur de pile comme opérande, elle a besoin de la vraie valeur. Si elle n'est pas dans le banc de registre, le séquenceur déclenche l'addition compteur-registre pour calculer la vraie valeur. Finalement, le banc de registre contient alors la bonne valeur et l'instruction peut s'exécuter sans encombre.
L'idée est que le pointeur de pile est généralement altéré par une série d'instruction PUSH/POP consécutives, puis des instructions LOAD/STORE/ADD/SUB utilisent le pointeur de pile final comme opérande. En clair, une bonne partie des incrémentations/décrémentation est accumulée dans le compteur delta, puis la vraie valeur est calculée une fois pour toutes et est utilisée comme opérande. On accumule un delta dans le compteur delta, et ce compteur delta est additionné quand nécessaire.
Précisons que le compteur delta est placé juste après le décodeur d'instruction, avant même le cache de micro-opération, l'unité de renommage et l'unité d'émission. Ainsi, les incrémentations/décrémentations du pointeur de pile disparaissent dès l'unité de décodage. Elles ne prennent pas de place dans le cache de micro-opération, ni dans la fenêtre d'instruction, ni dans la suite du pipeline. De nombreuses ressources sont économisées dans le ''front-end''.
Mais utiliser un ''stack engine'' a aussi de nombreux avantages au niveau du chemin de données/''back-end''. Déjà, sur les processeurs à exécution dans le désordre, cela libère une unité de calcul qui peut être utilisée pour faire d'autres calculs. Ensuite, le compteur delta mémorise un delta assez court, de 8 bits sur le processeur Pentium M, un peu plus pour les suivants. L'incrémentation se fait donc via un incrémenteur 8 bits, pas une grosse ALU 32/64 bits. Il y a un gain en termes de consommation d'énergie, un incrémenteur 8 bits étant moins gourmand qu'une grosse ALU 32/64 bits.
===Les points de synchronisation du delta===
La technique ne fonctionne que si la vraie valeur du pointeur de pile est calculée au bon moment, avant chaque utilisation pertinente. Il y a donc des '''points de synchronisation''' qui forcent le calcul de la vraie valeur. Plus haut, nous avions dit que c'était à chaque fois qu'une instruction adresse le pointeur de pile explicitement, qui l'utilise comme opérande. Les instructions CALL, RET, PUSH et POP ne sont pas concernées par elles utilisent le pointeur de pile de manière implicite et ne font que l'incrémenter/décrémenter. Mais dans les faits, c'est plus compliqué.
D'autres situations peuvent forcer une synchronisation, notamment un débordement du compteur delta. Le compteur delta est généralement un compteur de 8 bits, ce qui fait qu'il peut déborder. En cas de débordement du compteur, le séquenceur déclenche le calcul de la vraie valeur, puis réinitialise le compteur delta. La vraie valeur est donc calculée en avance dans ce cas précis. Précisons qu'un compteur delta de 8 bits permet de gérer environ 30 instructions PUSH/POP consécutives, ce qui rend les débordements de compteur delta assez peu fréquent. A noter que si le compteur delta vaut zéro, il n'y a pas besoin de calculer la vraie valeur, le séquenceur prend cette situation en compte.
Un autre point de synchronisation est celui des interruptions et exceptions matérielles. Il faut que le compteur delta soit sauvegardé lors d'une interruption et restauré quand elle se termine. Idem lors d'une commutation de contexte, quand on passe d'un programme à un autre. Pour cela, le processeur peut déclencher le calcul de la vraie valeur lors d'une interruption, avant de sauvegarder les registres. Pour cela, le processeur intègre un mécanisme de sauvegarde automatique, qui mémorise la valeur de ce compteur dans le tampon de réordonnancement, pour forcer le calcul de la vraie valeur en cas de problème.
La technique du ''stack engine'' est apparue sur les processeurs Pentium M d'Intel et les processeurs K10 d'AMD, et a été conservée sur tous les modèles ultérieurs. L'implémentation est cependant différente selon les processeurs, bien qu'on n'en connaisse pas les détails et que l'on doive se contenter des résultats de micro-benchmarks et des détails fournit par Intel et AMD. Selon certaines sources, dont les manuels d'optimisation d'Agner Fog, les processeurs AMD auraient moins de points de synchronisation que les processeurs Intel. De plus, leur ''stack engine'' serait placé plus loin que prévu dans le pipeline, après la file de micro-opération.
==La micro-fusion et la délamination==
La '''micro-fusion''' est une optimisation qui retarde le décodage réel de certaines instructions assez loin dans le pipeline. Elle concerne surtout les instruction ''load-op'', mais pas que. D'autres instructions mémoire peuvent subir cette optimisation. L'essentiel est que, sans micro-fusion, ces instructions sont décodée en plusieurs micro-opérations. La micro-fusion retarde le décodage final de ces instructions, qui se fait assez tard dans le pipeline. Pour comprendre l'intérêt, nous allons devoir faire quelques explications.
L'optimisation en question fait le décodage en deux temps. Prenons l'exemple d'une opération ''load-op'', qui est censée être décodée en deux micro-opérations : une lecture, puis une opération. Le décodage produit une '''macro-opération''' (terme d'Intel), qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. La macro-opération est techniquement une micro-opération comme une autre, sauf que ce n'est pas la micro-opération finale. L'unité d'émission découpe cette macro-opération en deux micro-opérations, qui sont émises l'une après l'autre. Pour résumer, les instructions ''load-op'' ne sont complétement découpées en micro-opération lors de l'émission, pas lors du décodage.
L'optimisation impacte ce qui se situe entre le décodeur d'instruction et l'unité d'émission : l'unité de renommage, la file de micro-opérations, le ''Loop Stream Detector'' et/ou le cache de micro-opération. L'intérêt de l'optimisation est que l'on fusionne deux micro-opérations en une seule macro-opération. Le cache de micro-opération et la file de micro-opération peuvent mémoriser plus de micro-opérations, vu que certaines sont fusionnées. Quant à l'unité de renommage, elle renomme une seule macro-opération au lieu de renommer deux micro-opérations, ce qui augmente le débit du renommage de registre. Par contre, cela complique un peu le travail de l'unité d'émission, qui doit découper les instructions ''load-op'' en deux et émettre les micro-opérations l'une après l'autre.
Il faut noter qu'il n'y a pas de fusion proprement dite. Le décodeur ne décode pas deux micro-opérations séparées, avant de les fusionner. En réalité, il génère directement une macro-opération, qui correspond à deux micro-opérations, que ce soit plus tard dans le pipeline ou sur une autre architecture. D'ailleurs, la micro-fusion a lieu à l'intérieur d'une instruction, qui est censée être traduite en plusieurs micro-opérations. Et encore : pas toutes. La conséquence est que la technique n'a de sens que sur les processeurs CISC, avec des instructions assez complexes pour être décodées en plusieurs micro-opérations. En pratique, seuls les CPU x86 sont concernés.
===Les domaines de fusion===
La micro-fusion n'est pas spécifique aux processeurs superscalaires. En théorie, on peut l'implémenter sur un processeur non-superscalaire à émission dans l'ordre. Cependant, je ne connais aucun processeur à émission dans l'ordre qui implémente la micro-fusion, même si son implémentation est en théorie possible sur de tels processeurs. La micro-fusion est implémentée différemment selon que l'émission se fasse dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
J'ai dit plus haut que les micro-opérations sont générées lors de l'émission, mais le moment exact varie suivant le processeur. Pour simplifier le tout, nous parlerons de '''scission''' quand le processeur traduit une macro-opération en deux/trois micro-opérations. Le moment où a lieu la scission est théoriquement dans l'unité d'émission, mais celle-ci prend plusieurs formes selon que le processeur utilise l'émission dans l'ordre, dans le désordre, avec une fenêtre d'instruction, avec des stations de réservation, etc.
Les processeurs à exécution dans l'ordre sont le cas le plus simple. L'unité d'émission est alors en charge du découpage des macro-opérations en micro-opérations. La file de micro-opération ou le cache de micro-opérations mémorisent des macro-opérations. À l'opposé, le tampon de ré-ordonnancement mémorise lui des micro-opérations. Le reste du processeur n'est pas affecté.
Sur les processeurs à exécution dans le désordre, les différences sont nombreuses. Le tampon de ré-ordonnancement peut mémoriser soit les micro-opérations finales, soit des macro-opérations. Il en est de même avec la fenêtre d'instruction et/ou les stations de réservation, qu'elles soient centralisées ou décentralisées. Tout dépend de comment se fait l'implémentation.
Une implémentation simple effectue la scission des macro-opérations dans l'unité d'émission, comme sur les processeurs à exécution dans l'ordre. Sur de nombreux processeurs, l'unité d'émission et de renommage sont simplement fusionnées, ce qui fait que la scission a lieu juste après le renommage de registres. En conséquence, le tampon de re-ordonnancement et les fenêtres d'instruction ne voient que des micro-opérations. Les macro-opérations disparaissent juste avant. Cette implémentation est préférée avec une fenêtre d'instruction centralisée, car on voit mal comment scinder une macro-opération juste avant l'ALU.
Une autre implémentation fait la scission en sortie de la fenêtre d'instruction ou des stations de réservation, qu'elles soient centralisées ou décentralisées. Dans ce cas, le tampon de re-ordonnancement fait de même. L'avantage est que le tampon de ré-ordonnancement stocke des macro-opérations, ce qui augmente sa taille effective. Par exemple, il utilise une seule entrée pour une instruction ''load-op'', au lieu de deux sans micro-fusion de ce type.
===La délamination===
La scission peut parfois avoir lieu plus tôt, dans des circonstances très précises. Concrètement, cela ne concerne que quelques microarchitectures d'Intel, pour des instructions très précises. La raison est les limitations du format des micro-opérations, pas une histoire d'optimisation. Le processeur aimerait pouvoir faire la micro-fusion, mais ne peut pas à cause de ces limitations. Une telle situation cause alors une '''délamination''' : les micro-opérations sont scindées précocement.
Par exemple, sur la microarchitecture SandyBridge d'Intel, les opérations ''load-op'' sont fusionnées, sauf si elles utilisent l'adressage indicé. Les micro-opérations de ce processeur ne peuvent avoir que deux opérandes. Trois opérandes est de trop, et les opérations ''load-op'' en adressage indicé dépassent cette limite. Dans ce cas, les micro-opérations sont fusionnées en sortie du décodeur, mais sont scindés avant d’entrer dans la file de micro-opération. L'avantage n'est pas évident, vu que l'unité de renommage suit de près le décodeur d'instruction. Il est que le cache de micro-opérations mémorise des macro-opérations, pas deux micro-opérations.
===La micro-fusion des processeurs Intel et AMD===
La micro-fusion est apparue sur le Pentium M d'Intel, et a été conservée telle quelle sur les processeurs Intel suivants. Les processeurs AMD supportent la micro-fusion depuis au moins la microarchitecture AMD K8. Mieux que ça, la délamination n'était pas utilisée car les micro-opérations acceptaient plus de 2 opérandes. Que ce soit chez AMD ou Intel, elle concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Sur le Pentium M, il y avait quelques limitations, l'optimisation se limitant par exemple aux registres entiers et flottants, mais pas aux registres MMX, mais pas les registres SSE (nous verrons ces registres dans quelques chapitres). Mais les processeurs modernes n'ont plus cette limitation. La micro-fusion des CPU Intel concerne deux types d'instructions : les écritures mémoire et les instructions ''load-op''.
Pour les écritures, la situation est plus complexe que pour les instructions ''load-op''. Sur les processeurs Intel précédents, les écritures étaient réalisées dans deux unités séparées : le calcul d'adresse dans une unité de calcul, l'écriture proprement dite dans l'unité mémoire. Elles prenaient donc deux micro-opérations. Le processeur a changé ses unités mémoire, ce qui fait qu'elle pouvait faire les deux en une seule micro-opération. Il s'agissait alors d'une optimisation du chemin de données, plus que de la micro-fusion proprement dite, mais je la mentionne ici malgré tout.
La micro-fusion sur ce processeur avait un avantage en plus des avantages usuels : elle utilisait mieux les décodeurs. Les Pentium M avaient trois décodeurs pour décoder plusieurs instructions en même temps, chose qu'on détaillera dans le chapitre sur les processeurs superscalaires. Un seul décodeur pouvait décoder toutes les instructions, les deux autres ne peuvent décoder que les instructions simples, qui se décodent en une seule micro-opération. Avec la micro-fusion, les trois décodeurs peuvent gérer les écritures mémoire et les opérations ''load-op'', vu qu'elles deviennent des instructions simples. Sans cela, elles auraient été limitées au premier décodeur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le parallélisme mémoire
| prevText=Le parallélisme mémoire
| next=Exemples de microarchitectures CPU : le cas du x86
| nextText=Exemples de microarchitectures CPU : le cas du x86
}}
</noinclude>
jacfpyw9ack83bie6w3ryitcwv8qt25
Fonctionnement d'un ordinateur/Les mémoires cache
0
65957
772485
765170
2026-09-19T20:16:38Z
Mewtow
31375
/* Le contrôleur de cache 82385 pour les CPU Intel 386 */
772485
wikitext
text/x-wiki
Le cache est une mémoire intercalée entre la mémoire et un processeur, plus rarement à l'intérieur d'un périphérique. Il est souvent fabriquée avec de la mémoire SRAM, parfois avec de l'eDRAM. Sans lui, on se croirait à l'âge de pierre tellement nos PC seraient lents ! En effet, la mémoire est très lente comparée au processeur. Le temps mis pour accéder à la mémoire est du temps durant lequel le processeur n'exécute pas d'instruction (sauf cas particuliers impliquant un pipeline). Pour diminuer ce temps d'attente, il a été décidé d'intercaler une mémoire petite mais rapide, entre le processeur et la mémoire. Ainsi, le processeur accède à un cache très rapide plutôt qu'à une RAM beaucoup plus lente.
==L'accès au cache==
Le cache contient une copie de certaines données présentes en RAM. La copie présente dans le cache est accessible bien plus rapidement que celle en RAM, vu que le cache est plus rapide. Mais seule une petite partie de ces données sont copiées dans le cache, les autres données devant être lues ou écrites dans la RAM. Toujours est-il que le cache contient une copie des dernières données accédées par le processeur.
Une donnée est copiée dans la mémoire cache quand elle est lue ou écrite par le processeur. Le processeur conserve une copie de la donnée dans le cache après son premier accès. Les lectures/écritures suivantes se feront alors directement dans le cache. Évidemment, au fur et à mesure des accès, certaines données anciennes sont éliminées du cache pour faire de la place aux nouveaux entrants, comme nous le verrons plus tard.
[[File:Principe d'une mémoire cache.gif|centre|vignette|upright=2|Principe d'une mémoire cache.]]
La mémoire cache est invisible pour le programmeur, qui ne peut pas déceler celles-ci dans l'assembleur. Les accès mémoire se font de la même manière avec ou sans le cache. La raison à cela est que le cache intercepte les accès mémoire et y répond s'il en a la capacité. Par exemple, si le cache intercepte une lecture à une adresse et que le contenu de cette adresse est dans le cache, le cache va outrepasser la mémoire RAM et la donnée sera envoyée par le cache au lieu d'être lue en RAM. par contre, si un accès se fait à une adresse pour laquelle le cache n'a pas la donnée, alors l'accès mémoire sera effectué par la RAM de la même manière que si le cache n'était pas là.
[[File:Accès au cache.png|centre|vignette|upright=2|Accès au cache]]
===Les succès et défauts de caches===
Tout accès mémoire est intercepté par le cache, qui vérifie si la donnée demandée est présente ou non dans le cache. Si la donnée voulue est présente dans le cache, on a un '''succès de cache''' (''cache hit'') et on accède à la donnée depuis le cache. Sinon, c'est un '''défaut de cache''' (''cache miss'') et on est obligé d’accéder à la RAM.
Les défauts de cache peuvent avoir plusieurs origines. Tout ce qu'il faut savoir est que lorsque le processeur accède à une donnée ou une instruction pour la première fois, il la place dans la mémoire cache car elle a de bonnes chances d'être réutilisée prochainement. La raison à cela est qu'un programme a tendance à réutiliser les instructions et données qui ont été accédées dans le passé : c'est le ''principe de localité temporelle''. Bien évidement, cela dépend du programme, de la façon dont celui-ci est programmé et accède à ses données et du traitement qu'il fait, mais c'est souvent vrai en général.
La première cause des défauts de cache est liée à la taille du cache. À force de charger des données/instructions dans le cache, le cache fini par être trop petit pour conserver les anciennes données. Le cache doit bien finir par faire de la place en supprimant les anciennes données, qui ont peu de chances d'être réutilisées. Ces anciennes données éliminées du cache peuvent cependant être accédées plus tard. Tout prochain accès à cette donnée mènera à un cache miss. C'est ce qu'on appelle un ''Capacity Cache Miss'', ou encore '''défaut de capacité'''. Les seules solutions pour éviter cela consistent à augmenter la taille du cache ou à optimiser le programme exécuté (voir plus bas).
Une autre raison pour un défaut est donc la suivante. Lorsqu'on exécute à une instruction ou qu'on accède à donnée pour la première fois, celle-ci n'a pas encore été chargée dans le cache. Le défaut de cache est inévitable : ce genre de cache miss s'appelle un ''Cold Miss'', ou encore un '''défaut à froid'''. De tels défauts sont presque impossibles à éliminer, sauf à utiliser des techniques de préchargement qui chargent à l'avance des données potentiellement utiles. Ces méthodes de préchargement se basent sur le principe de localité spatiale, à savoir le fait que les programmes ont tendance à accéder à des données proches en mémoire. Pour donner un exemple, les instructions d'un programme sont placées en mémoire dans l’ordre dans lequel on les exécute : la prochaine instruction à exécuter est souvent placée juste après l'instruction en cours (sauf avec les branchements). Quand on accède à une donnée ou une instruction, le cache peut précharger les données adjacentes pour en profiter. Nous parlerons de ces techniques de préchargement dans un chapitre dédié, vers la fin du cours.
===Le fonctionnement du cache, vu du processeur===
Vu du processeur, le cache prend en entrée toutes les informations nécessaires pour effectuer un accès mémoire : des signaux de commande, une adresse et la donnée à écrire si besoin. Tout cela est passé en entrée du cache, celui-ci répondant aux accès mémoire via divers bits de contrôles, que le processeur peut lire à souhait. Le cache fournit aussi la donnée à lire, pour les lectures, sur une sortie, connectée directement au bus mémoire/processeur. Globalement, le cache a une capacité limitée, mais il prend en entrée des adresses complètes. Par exemple, sur un processeur 64 bits, le cache prend en entrée des adresses de 64 bits (sauf si optimisations), même si le cache en question ne fait que quelques mébioctets.
Les caches sont souvent des mémoires multiports, surtout sur les processeurs récents. Les caches simple port sont rares, mêmes s'ils existent et ont existé par le passé. les caches double port sont eux plus fréquents, et ont généralement un port d'écriture séparé du port de lecture. Mais les caches récents ont plusieurs ports de lecture/écriture et sont capables de gérer plusieurs accès mémoire simultanés.
Les données présentes dans le cache sont (pré)chargées depuis la mémoire, ce qui fait que toute donnée dans le cache est la copie d'une donnée en mémoire RAM. Le cache doit faire la correspondance entre une donnée du cache et l'adresse mémoire correspondante. Du point de vue du fonctionnement, on peut voir le cache comme une sorte de table de correspondance, qui mémorise des données, chacune étant associée à son adresse mémoire. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Cela vaut du point de vue du processeur, le fonctionnement interne du cache étant quelque peu différent selon le cache. Il existe des caches dont le fonctionnement interne est bien celui d'une table de correspondance matérielle, d'autres qui sont beaucoup plus optimisés.
[[File:Fonctionnement d'une mémoire associative à correspondance.png|centre|vignette|upright=2|Fonctionnement simplifié d'une mémoire cache : les adresses sont dans la colonne de gauche, les données sont dans la colonne de droite. On voit qu'on envoie l'adresse au cache, que celui-ci répond en renvoyant la donnée associée.]]
==La performance des mémoires caches==
L'analyse de la performance des mémoires caches est plus riche pour celle des autres mémoires. Sa performance dépend de beaucoup de paramètres, mais on peut cependant citer les principaux. Les deux premiers sont tout bonnement sa latence et son débit, comme pour n'importe quelle autre mémoire. La latence est plus importante que son débit, car le processeur est généralement plus rapide que le cache et qu'il n'aime pas attendre. Mais le critère le plus important pour un cache est sa capacité à empêcher des accès mémoire, son efficacité. Plus les accès mémoire sont servis par le cache au lieu de la RAM, meilleures seront les performances. Pour résumer, la performance d'un cache est surtout caractérisée par deux métriques : le taux de défaut, qui correspond à l’efficacité du cache, et la latence du cache.
===Le taux de succès/défaut===
Le '''taux de succès''' (hit ratio) est un premier indicateur des performances du cache, mais un indicateur assez imparfait. C'est le pourcentage d'accès mémoire qui ne déclenchent pas de défaut de cache. Plus il est élevé, plus le processeur accède au cache à la place de la RAM et plus le cache est efficace. Certains chercheurs préfèrent utiliser le '''taux de défauts''', à savoir le pourcentage d'accès mémoire qui entraînent un défaut de cache. Plus il est bas, meilleures sont les performances. Le taux de défaut est relié au taux de succès par l'équation <math>T_\text{succes} = 1 - T_\text{defaut}</math>. Par définition, il est égal à :
: <math>\text{Taux de défauts de cache} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d’accès mémoires}}</math>
Plutôt que de comparer le nombre de défauts/succès de cache au nombre d'accès mémoire, il est aussi possible de diviser le nombre de défauts par le nombre total d'instructions. On obtient alors le '''taux de défauts/succès par instruction''', une autre métrique utile. Par définition, elle est égale à :
: <math>\text{Taux de défauts par instruction} = \frac{\text{Nombre de défauts de cache}}{\text{Nombre d'instructions}} = \text{Taux de défauts de cache} \times \frac{\text{Nombre d’accès mémoires}}{\text{Nombre d'instructions}}</math>
Si certains défauts de cache sont inévitables quel que soit le cache, comme les défauts à froids, mentionnés plus haut, d'autres défauts peuvent être évités en augmentant la capacité du cache. C'est le cas des défauts de capacité qui sont causés par un accès à une donnée qui a été éliminée du cache faute de place. Plus le cache est gros, moins il a de chances d'être rempli, moins il doit rapatrier de données, plus son taux de succès augmente. Mais nous reviendrons sur le lien entre taille du cache et taux de défaut plus bas.
Le taux de succès ne dépend pas que du cache, mais aussi de la conception des programmes exécutés. Une bonne utilisation du cache (ainsi que de la mémoire virtuelle) repose sur le programmeur qui doit prendre en compte les principes de localités dès la conception de ses programmes.
Par exemple, un programmeur peut parfaitement tenir compte du cache au niveau de son algorithme : on peut citer l'existence des algorithmes ''cache oblivious'', qui sont conçus pour être optimaux quelle que soit la taille du cache. Le programmeur peut aussi choisir ses structures de données de manière à améliorer la localité. Par exemple, un tableau est une structure de donnée respectant le principe de localité spatiale, tandis qu'une liste chaînée ou un arbre n'en sont pas (bien qu'on puisse les implémenter de façon à limiter la casse). D'autres optimisations sont parfois possibles : par exemple, le sens de parcours d'un tableau multidimensionnel peut faire une grosse différence. Cela permet des gains très intéressants pouvant se mesurer avec des nombres à deux ou trois chiffres.
Je vous recommande, si vous êtes programmeur, de vous renseigner le plus possible sur les optimisations de code ou algorithmiques qui concernent le cache : il vous suffira de chercher sur Google. Il y a une citation qui résume bien cela, prononcée par un certain Terje Mathisen. Si vous ne le connaissez pas, cet homme est un vieux programmeur (du temps durant lequel on codait encore en assembleur), grand gourou de l’optimisation, qui a notamment travaillé sur le moteur de Quake 3 Arena.
{{BlocCitation|Almost all programming can be viewed as an exercise in caching.|auteur=Terje Mathisen}}
===La latence moyenne d'un cache===
Le temps mis pour lire ou écrire une donnée varie en présence d'un cache. Certaines lectures/écritures vont atterrir directement dans le cache (succès) tandis que d'autres devront aller chercher leur contenu en mémoire RAM (défaut de cache). Dans tous les cas, qu'il y ait défaut ou non, le cache sera consulté et mettra un certain temps à répondre, égal au temps de latence du cache. Tous les accès mémoires auront donc une durée au moins égale au temps de latence du cache, qui sera notée <math>T_c</math>.
En cas de succès, le cache aura effectué la lecture ou l'écriture, et aucune action supplémentaire n'est requise. Ce qui n'est pas le cas en cas de défaut : le processeur devra aller lire/écrire la donnée en RAM, ce qui prend un temps supplémentaire égal au temps de latence de la mémoire RAM. Un défaut ajoute donc un temps, une pénalité, à l'accès mémoire. Dans ce qui suivra, le temps d'accès à la RAM sera noté <math>T_m</math>. Fort de ces informations, nous pouvons calculer le temps de latence moyen d'un accès mémoire, qui est la somme du temps d'accès au cache (pour tous les accès mémoire), multiplié par le temps lié aux défauts. On a alors :
: <math>T = T_c + \text{Taux de défaut} \times T_m</math>
On voit que plus le taux de succès est élevé, plus le temps de latence moyen sera bas, et inversement. Ce qui explique l'influence du taux de succès sur les performances du cache, influence assez importante sur les processeurs actuels. De nos jours, le temps que passe le processeur dans les défauts de cache devient de plus en plus un problème au fil du temps, et gérer correctement le cache est une nécessité, particulièrement sur les processeurs multi-cœurs.
Il faut dire que la différence de vitesse entre processeur et mémoire est tellement importante que les défauts de cache sont très lents : alors qu'un succès de cache va prendre entre 1 et 5 cycles d'horloge, un cache miss fera plus dans les 400-1000 cycles d'horloge. Tout ce temps sera du temps de perdu que le processeur aura du mal à mitiger. Autant dire que réduire les défauts de cache est beaucoup plus efficace que d'optimiser les calculs effectués par le processeur (erreur courante chez de nombreux programmeurs, notamment débutants).
===L'impact de la taille du cache sur le taux de défaut et la latence===
Il y a un lien entre taille du cache, taux de défaut, débit binaire et latence moyenne. Globalement, plus un cache est gros, plus il est lent. Simple application de la notion de hiérarchie mémoire vue il y a quelques chapitres. Les raisons à cela sont nombreuses, mais nous ne pouvons pas les aborder ici, car il faudrait que nous sachions comment fonctionne un cache et ce qu'il y a à l'intérieur, ce qui sera vu dans la suite du chapitre. Toujours est-il que la latence moyenne d'un cache assez gros est assez importante. De même, le débit binaire d'un cache diminue avec sa taille, mais dans une moindre mesure. Les petits caches ont donc un gros débit binaire et une faible latence, alors que c'est l'inverse pour les gros caches.
Une grande capacité de cache améliore le taux de succès, mais cela se fait au détriment de son temps de latence et de son débit, ce qui fait qu'il y a un compromis assez difficile à trouver entre taille du cache, latence et débit. Il peut arriver qu'augmenter la taille du cache augmente son temps d'accès au point d’entraîner une baisse de performance. Par exemple, les processeurs Nehalem d'Intel ont vus leurs performances dans certains jeux vidéos baisser de 2 à 3 %, malgré de nombreuses améliorations architecturales, parce que la latence du cache L1 avait augmentée de 2 cycles d'horloge.
Pour avoir une petite idée du compromis à faire, regardons la relation entre taille du cache et taux de défaut. Il existe une relation approximative entre ces deux variables, appelée la '''loi de puissance des défauts de cache'''. Elle donne le nombre total de défaut de cache en fonction de la taille du cache et de deux autres paramètres. Voici cette loi :
: <math>\text{Taux de défauts de cache} \approx K \times \text{Taille du cache}^{- \alpha }</math>, avec <math>K</math> et <math>\alpha</math> deux coefficients qui dépendent du programme exécuté.
Le coefficient <math>\alpha</math> est généralement compris entre 0.3 et 0.7, guère plus, et varie suivant le programme exécuté. Précisons que cette loi ne marche que si le cache est assez petit par rapport aux données à utiliser. Pour un cache assez gros et des données très petites, la relation précédente est mise en défaut. Pour s'en rendre compte, il suffit d'étudier le cas extrême où toutes les données nécessaires tiennent dans le cache. Dans ce cas, il n'y a qu'un nombre fixe de défauts de cache : autant qu'il faut charger de données dans le cache. Le nombre de défauts de cache observé dans cette situation n'est autre que le coefficient <math>K</math> de la situation précédente, mais il n'y a aucune dépendance entre taux de défaut et taille du cache.
L'origine de cette relation s'explique quand on regarde combien de fois chaque donnée est réutilisée lors de l’exécution d'un programme. La plupart des données finissent par être ré-accédées à un moment ou un autre et il se passe un certain temps entre deux accès à une même donnée. Sur la plupart des programmes, les observations montrent que beaucoup de réutilisations de données se font après un temps très court et qu'inversement, peu de ré-accès se font après un temps inter-accès long. Si on compte le nombre de réutilisation qui ont un temps inter-accès bien précis, on retrouve une loi de puissance identique à celle vue précédemment :
: <math>\text{Nombre de réaccès avec un temps inter-accès égal à t} \approx K \times t^{- \beta}</math>, avec t le temps moyen entre deux réutilisations.
Le coefficient <math>\beta</math> est ici compris entre 1.7 et 1.3. De manière générale, les coefficients <math>\alpha</math> et <math>\beta</math> sont reliés par la relation <math>\alpha = 1 - \beta</math>, ce qui montre qu'il y a un lien entre les deux relations.
Précisons cependant que la loi de puissance précédente ne vaut pas pour tous les programmes informatiques, mais seulement pour la plupart d’entre eux. Il n'est pas rare de trouver quelques programmes pour lesquels les accès aux données sont relativement prédictibles et où une bonne optimisation du code fait que la loi de puissance précédente n'est pas valide.
La loi de puissance des défauts de cache peut se démontrer à partir de la relation précédente, sous certaines hypothèses. Si un suppose que le cache est assez petit par rapport aux données, alors les deux relations sont équivalentes. L'idée qui se cache derrière la démonstration est que si le temps entre deux accès à une donnée est trop long, alors la donnée accédée aura plus de chance d'être rapatriée en RAM, ce qui cause un défaut de cache. La chance de rapatriement dépend de la taille du cache, un cache plus gros peut conserver plus de données et a donc un temps avant rapatriement plus long.
==Les lignes de cache et leurs tags==
Du point de vue du processeur, les lectures et écritures se font mot mémoire par mot mémoire. Un processeur avec des entiers de 64 bits recoit des données de 64 bits de la part du cache, et y écrit des mots de 64 bits. Mais quand on regarde comment sont stockées les données à l'intérieur du cache, les choses sont différentes.
===Les lignes de cache===
Les données sont mémorisées dans le cache par blocs de plusieurs bytes, d'environ 64 à 256 octets chacun, qui portent le nom de '''lignes de cache'''. Les lignes de cache sont l'unité de stockage que l'on trouve à l'intérieur du cache, mais elles servent aussi d'unité de transaction avec la mémoire RAM. Sur les caches actuels, on transfère les données entre le cache et la RAM ligne de cache par ligne de cache, dans la limite de la taille du bus mémoire. Mais d'autres caches plus anciens permettaient de faire des transferts plus fins. C’est-à-dire qu'on pouvait mettre à jour quelques octets dans une ligne de cache sans avoir à la recopier intégralement depuis ou dans la mémoire RAM.
En théorie, on pourrait imaginer des caches où les données sont stockées différemment, où l'unité serait le mot mémoire, par exemple. Par exemple, sur un processeur 64 bits, on aurait une ligne de cache de 64 bits. Cela aurait l'avantage de la simplicité : les transferts entre le processeur et la mémoire serait de même taille, l'intérieur du cache ressemblerait à son interface montrée au processeur. Mais cela aurait quelques défauts qui sont compensés par l'organisation en lignes de cache de grande taille.
Le premier avantage des lignes de cache est lié à la localité spatiale, la tendance qu'on les programmes à accéder à des données proches les unes des autres. Des accès mémoires consécutifs ont tendance à se faire à des adresses proches, qui ont de bonnes chances d'être dans la même ligne de cache. Et des accès consécutifs à une même ligne de cache sont plus rapides que des accès à deux lignes distinctes. Une autre raison est tout simplement que cela simplifie considérablement la circuiterie du cache. Pour une capacité identique, il vaut mieux avoir peu de lignes de cache assez grosses, que beaucoup de petites lignes de cache. La raison est que les circuits du cache, comme le décodeur, l'encodeur et autres, ont moins de sorties et sont donc plus simples.
===L'alignement des lignes de cache===
Les lignes de cache sont des blocs de plusieurs dizaines à centaines de bytes, dont la taille est presque toujours une puissance de deux. De plus, les lignes de cache sont alignées en mémoire. Nous avions déjà abordé la notion d'alignement mémoire dans un chapitre précédent, mais le concept d'alignement des lignes de cache est quelque peu différent. Quand nous avions parlé d'alignement auparavant, il s'agissait de l'alignement des données manipulées par le processeur, qui faisait partie du jeu d'instruction du processeur. Ici, nous parlons d'un alignement totalement différent, invisible pour le programmeur, sans lien avec le jeu d’instruction. Voyons de quoi il retourne.
Concrètement, cela veut dire que du point de vue du cache, la RAM est découpée en blocs qui font la même taille qu'une ligne de cache, aux positions prédéterminées, sans recouvrement entre les blocs. Par exemple, pour un cache dont les lignes de cache font 256 octets, le premier bloc est à l'adresse 0, le second est 256 octets plus loin, c'est à dire à l'adresse 256, le troisième à l'adresse 512, la quatrième à l'adresse 768, etc. Une ligne de cache de 256 octets contiendra une donnée provenant d'un bloc de RAM de 256 octets, dont l'adresse est systématiquement un multiple de 256. Il n'est pas possible qu'une ligne de cache contienne un bloc de 256 octets dont l'adresse du premier octet serait l'adresse 64, ou l'adresse 32, par exemple. En clair, les adresses de ces blocs sont des multiples de la taille de la ligne de cache, de la taille des blocs. Cela rappelle les contraintes d'alignement vues dans le chapitre "Le modèle mémoire : alignement et boutisme", mais appliquées aux lignes de cache.
L'alignement des lignes de cache a des conséquences pratiques pour la conception des caches. Notons qu'il est en théorie possible d'avoir des caches dont les lignes de cache ne sont pas alignées, mais cela poserait des problèmes majeurs. Il serait en effet possible qu'une donnée soit présente dans deux lignes de cache à la fois. Par exemple, prenons le cas où une ligne de cache de 256 commence à l'adresse 64 et une autre ligne de cache commence à l'adresse 0. L'adresse 128 serait dans les deux lignes de cache ! Et cela poserait des problèmes lors des lectures, mais encore plus lors des écritures. C'est pour éviter ce genre de problèmes que les lignes de cache sont alignées avec la mémoire RAM dans tous les caches existants.
L'alignement des lignes de cache est une chose que les programmeurs doivent parfois prendre en compte quand ils écrivent du code ultra-optimisé, destiné à des programmes demandant des performances extrêmes. Il arrive que les contraintes d'alignement posent des problèmes. Nous avions vu dans le chapitre sur le boutisme et l'alignement qu'il valait mieux gérer l'alignement des variables des structures de données, pour éviter les accès non-alignés avec le bus mémoire. La même chose est possible, mais pour l'alignement avec des lignes de cache. Typiquement, l'idéal est que, pour une structure de donnée, on puisse en mettre un nombre entier dans une ligne de cache. Ou alors, si la structure est vraiment grande, que celle-ci occupe un nombre entier de lignes de cache. Si ce n'est pas le cas, il y a un risque d'accès non-alignés, c'est à dire qu'une structure se retrouve à cheval sur deux lignes de cache, avec les défauts que cela implique.
===Le tag d'une ligne de cache===
Plus haut, nous avions dit que le cache mémorise, pour chaque ligne de cache, l'adresse RAM associée. Le cache contient donc des paires adresse-ligne de cache qui lui permettent de faire le lien entre ligne de cache et adresse. Mais du fait de l'organisation du cache en lignes de cache de grande taille, qui sont de plus alignées en mémoire, il faut nuancer cette affirmation. Le cache ne mémorise pas la totalité de l'adresse, ce qui serait inutile. L'alignement des lignes de cache en RAM fait que les bits de poids faible de l'adresse ne sont pas à prendre en compte pour l'association adresse-ligne de cache. Dans ces conditions, on mémorise seulement la partie utile de l'adresse mémoire correspondante, qui forme ce qu'on appelle le '''tag'''.
Le reste de l'adresse indique quelle est la position de la donnée dans la ligne de cache. Par exemple, prenons le cas où le processeur gère des nombres entiers de 64 bits (8 octets) et des lignes de cache de 128 octets : chaque ligne de cache contient donc 16 entiers. Si le processeur veut lire ou écrire un entier bien précis, il doit préciser sa place dans la ligne de cache. Et ce sont les bits de l'adresse mémoire non-inclus dans le cache qui permettent de faire ça. En clair, une adresse mémoire à lire/écrire est interprété par le cache comme la concaténation d'un tag et de la position de la donnée dans la ligne de cache correspondante.
[[File:Adressage d'un cache totalement associatif.png|centre|vignette|upright=2|Adressage d'un cache totalement associatif]]
Le cache est donc une grande table de correspondance entre tags et lignes de cache. Lors d'un accès mémoire, le cache extrait le tag de l'adresse à lire ou écrire, et le compare avec les tags de chaque ligne de cache. Si une ligne contient ce tag, alors c'est que cette ligne correspond à l'adresse, et c'est un défaut de cache sinon. Lors d'un succès de cache, la ligne de cache est lue depuis le cache et envoyée à un multiplexeur qui sélectionne la donnée à lire dans la ligne de cache. Le fonctionnement est similaire pour une écriture : la donnée à écrire passe dans un démultiplexeur, qui envoie la donnée au bon endroit dans la ligne de cache sélectionnée.
[[File:Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.png|centre|vignette|upright=2|Lecture d'une donnée dans un cache CPU, organisé en lignes de cache.]]
===Le contenu d'une ligne de cache===
Dans ce qui va suivre, nous allons considérer que chaque ligne de cache mémorise son tag, les données de la ligne de cache proprement dit, et quelques bits de contrôle annexes qui varient suivant le cache considéré.
[[File:Tag d'une ligne de cache.png|centre|vignette|upright=2|Tag d'une ligne de cache.]]
Les caches modernes incluent de nombreux bits de contrôle, mais deux d'entre eux sont communs à presque tous les caches modernes : le bit ''Dirty'' et le bit ''Valid''.
Le '''bit ''Valid''''' indique si la ligne de cache contient des données valides ou non. Si le bit ''Valid'' est à 0, la ligne de cache est en état valide, à savoir qu'elle contient des données et n'est pas vide. Par contre, si ce bit est à 1, la ligne de cache est invalide et son contenu ne peut pas être lu ou écrit. L'utilité de ce bit est qu'il permet d'effacer une ligne de cache très rapidement : il suffit de mettre ce bit à 0. Il existe des situations où le cache doit être effacé, on dit alors qu'il est invalidé. Une section de ce chapitre sera dédié à l'invalidation du cache.
Le '''bit ''Dirty''''' indique qu'une ligne de cache a été modifiée. Par modifiée, on veut dire que le processeur a écrit dedans, qu'il a modifié la ligne de cache. Mais attention : si la donnée a été modifiée dans le cache, la modification n'est pas forcément propagée en mémoire RAM. Le bit ''dirty'' indique si c'est le cas, si l'écriture a été propagée en mémoire RAM. Il précise que la ligne de cache contient des données modifiées, alors que la RAM a des données initiales non-modifiées. Une ligne de cache avec un bit ''dirty'' à 1 est dite ''dirty'', par métonymie. Nous verrons cela en détail dans la section sur les caches ''write-back'' et ''write-through''.
Les caches modernes ajoutent des '''bits de détection/correction d'erreur''' dans les bits de contrôle. Pour rappel, les codes de détection/correction d'erreur permettent de se prémunir contre des erreurs matérielles, qui corrompent les données stockées dans une mémoire, ici une mémoire cache. Ils ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Nous reviendrons dessus dans une section ultérieur de ce chapitre.
Sur certains caches assez anciens, on pouvait transférer les lignes de caches morceaux par morceaux. Ces caches avaient des lignes de cache divisées en sous-secteurs, ces sous-secteurs étant des morceaux de ligne de cache qu'on pouvait charger indépendamment les uns des autres (mais qui sont consécutifs en RAM). Chaque secteur avait ses propres bits de contrôle, mais le tag était commun à tous les secteurs.
[[File:Cache à secteurs.png|centre|vignette|upright=2.5|Cache à secteurs.]]
: Dans ce qui va suivre, le terme "ligne de cache" désignera soit un bloc de données copiées depuis la RAM d'une taille de 64/128/256/... octets, soit la concaténation de ces données avec le tag et des bits de contrôle. Les deux définitions ne sont pas équivalentes, mais l'usage a entériné cet abus de langage. Et il faut avouer que cela rend les explications du chapitre plus simples.
==Les instructions de contrôle du cache==
Plus haut, nous avions dit que le cache est totalement transparent du point de vue du programmeur. Le cache contient des copies de données en RAM, le programmeur n'a rien à faire pour utiliser le cache correctement. Mais la réalité est que pour des raisons diverses, des processeurs incorporent des '''instructions de contrôle du cache'''. Il s'agit d’instructions qui agissent sur le contenu du cache. Elles existent pour des raisons diverses qu'on détaillera plus bas, mais il s'agit globalement d'une question de performances ou de nécessité pour le système d'exploitation.
===Les instructions de préchargement===
La première instruction de contrôle du cache est une '''instruction de préchargement''', qui demande à charger un bloc de données dans le cache. Elle prend en opérande une adresse mémoire, et le contenu de cette adresse est chargé dans une ligne de cache. Bien sûr, des contraintes d'alignement sont à prendre en compte : on charge un bloc de la même taille qu'une ligne de cache, aligné en mémoire sur la taille du bloc, qui contient l'adresse.
L'instruction de préchargement n'est utile que si l'instruction est exécutée bien avant que la donnée ne soit utilisée/lue/écrite. Cela permet de charger une donnée dans le cache à l'avance, d'où le nom de préchargement donné à cette technique. Mais les processeurs modernes gérent des techniques de préchargement automatique, qui ne requièrent pas d'instructions de préchargement. Le préchargement automatique et les instructions de préchargement sont deux solutions complémentaires, mais qui peuvent se marcher sur les pieds. Nous en reparlerons dans le prochain chapitre, qui sera dédié au préchargement automatique.
Il faut noter que les instructions de préchargement peuvent être ignorées par le processeur. Sous certaines conditions, le processeur peut décider que l'instruction de préchargement ne sera pas exécutée. Par exemple, il ne va pas précharger une donnée déjà présente dans le cache. Ou encore, si le bus mémoire est occupé, il ne va pas exécuter le préchargement, par manque de ressources matérielles.
===Les instructions d'invalidation et de ''flush''===
Les instructions ''flush'' regroupent deux types d'instructions qui sont souvent utilisées en même temps. Il s'agit des instructions d'invalidation et de nettoyage (''clean''). Les deux termes proviennent de la terminologie ARM, il n'y a pas de terminologie standardisé pour les noms de ces instructions.
Dans les grandes lignes, elles permettent de vider le cache, à savoir de rapatrier son contenu en RAM et de réinitialiser le cache à zéro. Elles sont utilisées par le système d'exploitation lors des commutations de contexte, à savoir quand on passe d'un programme à un autre. Elles sont aussi utilisées lors des appels systèmes et routines d'interruption/exception. L'idée est de vider le cache avant d'exécuter un nouveau programme ou une nouvelle routine. Le nouveau programme aura accès à un cache tout propre, les données de l'ancien programme auront été retirée du cache.
Les '''instructions ''clean''''' recopient le contenu de la ligne de cache en RAM. Elles forcent la recopie immédiatement de la ligne de cache en mémoire RAM. Pour faire leur travail, elle vérifient si la ligne de cache a été modifiée, avant de la recopier en RAM. Et pour cela, ils vérifient le bit de contrôle ''dirty'', qui est mis à 1 après une première écriture. Si ce bit est à 0, alors pas besoin de recopier la ligne de cache : elle n'a pas été modifiée, la RAM a déjà la bonne copie. Mais s'il est à 1, le cache et la RAM n'ont pas le même contenu, la recopie s'exécute.
Les '''instructions d'invalidation''' permettent d'invalider une ligne de cache, à savoir d'effacer son contenu. Nous verrons à quoi servent ces instructions dans la section sur les changement de processus. Invalider une ligne de cache est une opération optimisée : le cache n'est en réalité pas réellement effacé. À la place, le bit ''Valid'' de chaque ligne de cache est juste mis à 0. Il faut noter que l'invalidation efface les lignes de cache sans se préoccuper de leur contenu. Elle se moque qu'une ligne de cache contienne une donnée modifiée, ''dirty'' ou quoique ce soit : la ligne de cache est effacée, point.
Il est possible d'invalider une ligne de cache en fournissant une adresse mémoire, mais il est aussi possible d'invalider le cache tout entier. Le choix entre les deux dépend du mode d'adressage de l'instruction d'invalidation. Parfois, il existe une instruction séparée pour invalider tout le cache, et une autre pour invalider une ligne de cache bien précise. Des instructions séparées sont parfois disponibles pour invalider les caches de données et d'instructions, parfois aussi la TLB (un cache qu'on verra dans quelques chapitres). Il est possible de n'invalider que le cache L1, voire le cache L2.
Il faut noter que l'invalidation efface tout le cache, mais ne se préoccupe pas de vérifier si les données ont été modifiées dans le cache. Pour certains caches, comme le cache d'instruction, ce n'est pas un problème, vu qu'il est en "lecture seule". Mais pour les caches de données, les données modifiées sont perdues en cas d'invalidation. Heureusement, il existe des instructions d'invalidation qui fusionnent une instruction ''clean'' et une instruction d'invalidation. Il s'agit d''''instructions d'invalidation spéciales'''.
===Les instructions d'optimisation : instructions non-temporelles et écritures optimisées===
Les '''instructions mémoire non-temporelles''' contournent complètement le cache. Par exemple, une lecture peut lire une donnée, mais celle-ci ne sera pas chargée dans le cache, elle passe directement de la RAM vers les registres. Une section entière de ce chapitre sera dédiée au contournement du cache, à savoir aux situations où les accès mémoire doivent passer directement du processeur à la RAM sans passer par le cache.
D'autres instructions assez rares incorporent des indications pour le cache. Par exemple, l'instruction ''load last'' des processeurs POWER PC implique que la donnée ne sera utilisée qu'une seule fois. Elle est donc chargée dans le cache, mais la ligne de cache est configurée de manière à être remplacée très rapidement, typiquement avec une valeur de LRU/LFU adéquate. La donnée est bien chargée dans le cache, au cas où elle doive être relue suite à une mauvaise prédiction de branchement ou autre, chose qu'une lecture non-temporelle (qui contourne le cache) ne fait pas. Des indications de ce type sont appelées des '''''cache hint'''''.
L''''instruction ''flush''''' permet de préciser qu'une ligne de cache contient une donnée inutile, qui ne sera pas réutilisée par le programme. Pas besoin de la conserver dans le cache, elle peut laisser sa place à des données plus utiles. Or, sans indication, les algorithmes de remplacement d'une ligne de cache risquent de conserver cette donnée trop longtemps, ce qui entraine une certaine pollution du cache par des données inutiles.
Une autre instruction est elle beaucoup plus importante : celle de '''pré-allocation sur écriture'''. Elle sert dans le cas où une ligne de cache est complétement écrite. Par exemple, imaginons qu'on veuille écrire dans une portion de mémoire. Si celle-ci n'est pas dans le cache, le processeur va charger une ligne de cache complète depuis la RAM, écrire dans la ligne de cache, puis recopier la ligne de cache modifiée en mémoire RAM. Une écriture en RAM demande donc de faire une lecture et une écriture. Mais les instructions de pré-allocation sur écriture permettent de prévenir qu'une ligne de cache sera intégralement écrite, et qu'il n'y a donc pas besoin de lire celle-ci depuis la RAM. Notons que l'instruction d'écriture qui suit n'est pas une écriture non-temporelle, vu que les données sont écrites dans la ligne de cache, qui est ensuite envoyée en mémoire RAM dès que nécessaire. De plus, les données écrites peuvent ensuite être relue depuis le cache si nécessaire.
Enfin, certains processeurs MIPS incorporent une instruction pour modifier le tag d'une ligne de cache. Elles servent à optimiser les copies mémoire, à savoir quand on copie un bloc de données d'un endroit à un autre. L'idée est de charger le bloc de données dans le cache avec une instruction LOAD/PREFETCH, de modifier le tag pour qu'il pointe vers l'adresse à écrire, et de laisser faire le cache pour que l'écriture se fasse en RAM. Mais les contraintes pour utiliser cette instruction sont assez drastiques : les données doivent être alignées sur la taille d'une ligne de cache, le bloc de départ et d'arrivée (l'original versus la copie) ne doivent pas se recouvrir, etc.
==L'associativité des caches et leur adressage implicite==
Lorsqu'on souhaite accéder au cache, il faut trouver quelle est la ligne de cache dont le tag correspond à l'adresse demandée. On peut classifier les caches selon leur stratégie de recherche de la ligne correspondante en trois types de caches : totalement associatifs, directement adressés (''direct mapped'') et associatifs par voie.
===Les caches totalement associatifs===
Avec les caches totalement associatifs, toute donnée chargée depuis la mémoire peut être placée dans n'importe quelle ligne de cache, sans aucune restriction. Ces caches ont un taux de succès très élevé, quand on les compare aux autres caches.
[[File:Cache totalement associatif.png|centre|vignette|upright=2|Cache totalement associatif.]]
Concevoir un cache totalement associatif peut se faire de deux grandes manières différentes. La première consiste tout simplement à combiner une mémoire associative avec une mémoire RAM, en ajoutant éventuellement quelques circuits annexes. La mémoire associative mémorise les tags, alors que la mémoire RAM mémorise les données de la ligne de cache, éventuellement avec quelques bits de contrôle. La ligne de cache est stockée à une adresse A dans la mémoire RAM et son tag est stocké à la même adresse, mais dans la mémoire CAM. Ce faisant, quand on envoie le tag à la mémoire CAM, elle renvoie l'adresse de la ligne de cache dans la mémoire RAM. Cette adresse est alors envoyée directement sur le bus d'adresse de la RAM, et la lecture est effectuée automatiquement. Il faut ajouter quelques circuits annexes pour garantir que les écritures se passent correctement dans les deux mémoires, mais rien de bien terrible.
[[File:Cache fabriqué avec une mémoire associative et une RAM.png|centre|vignette|upright=3|Cache fabriqué avec une mémoire associative et une RAM]]
Il est cependant possible d'optimiser un tel cache, en fusionnant la mémoire CAM et la mémoire RAM, afin d'éliminer des circuits redondants. Pour comprendre pourquoi, rappelons que les mémoires CAM sont composées d'un plan mémoire, d'un paquet de comparateurs et d'un encodeur. Quant à la mémoire RAM, elle est composée d'un décodeur connecté au plan mémoire. En mettant une CAM suivie d'une RAM, on a un encodeur dont l'entrée est envoyée à un décodeur.
[[File:Cache totalement associatif naif.png|centre|vignette|upright=3|Cache totalement associatif naif]]
Or, le décodeur réalise l'opération inverse de l'encodeur, ce qui fait que mettre les deux composants à la suite ne sert à rien. On peut donc retirer l'encodeur et le décodeur, et envoyer directement les résultats des comparateurs sur les entrées de commande du plan mémoire de la RAM.
[[File:Cache totalement associatif optimisé.png|centre|vignette|upright=2|Cache totalement associatif optimisé]]
Avec cette méthode, les circuits du cache ressemblent à ce qui illustré ci-dessous. Le tag est envoyé à chaque ligne de cache. Le tag envoyé est alors comparé avec le Tag contenu dans chaque ligne de cache, comme c'est le cas sur les mémoires associatives. Si une ligne de cache matche avec le tag envoyé en entrée, la ligne pour laquelle il y a eu une égalité est alors connectée sur les lignes de bit (''bitlines''). Cela est réalisé par un circuit commandé par le comparateur de la ligne de cache. Il ne reste plus qu'à sélectionner la portion de la ligne de cache qui nous intéresse, grâce à un paquet de multiplexeurs. Cela permet d'effectuer une lecture ou écriture, mais il faut aussi préciser si il y a eu un défaut de cache ou un succès. Un succès de cache a lieu quand au moins des comparaisons est positive, alors que c'est un défaut de cache sinon. En clair, détecter un succès de cache demande juste de connecter une porte OU à plusieurs entrées à tous les comparateurs.
[[File:Organisation générale d'un cache totalement associatif.png|centre|vignette|upright=2|Organisation générale d'un cache totalement associatif.]]
===Les caches directement adressés===
Les caches directement adressés peuvent être vus comme un cache totalement associatif auquel on aurait ajouté des restrictions assez drastiques. Plus haut, on a vu qu'un cache totalement adressé est équivalent à la combinaison d'une CAM avec une RAM. La mémoire CAM prend en entrée un Tag et traduit celui-ci en une adresse qui commande la mémoire RAM interne au cache. Dans ce qui suit, l'adresse interne au cache sera appelé l''''indice''' pour éviter toute confusion.
[[File:Cache hash table - 2.png|centre|vignette|upright=2|Fonctionnement interne du cache, expliquée sous forme abstraite, en utilisant la notion d'indice interne au cache.]]
Les caches directement adressés cherchent à remplacer la mémoire CAM par un circuit combinatoire. Ce circuit traduit le Tag en indice, mais est beaucoup plus simple qu'une mémoire CAM. Mais qui dit circuit plus simple dit circuit plus limité. Un circuit combinatoire n'est pas aussi versatile que ce qui est permis avec une mémoire CAM. En conséquence, une restriction majeure apparait : toute adresse mémoire est associée dans une ligne de cache prédéfinie, toujours la même. L'association entre ligne de cache et adresse mémoire est faite par le circuit combinatoire, et ne peut pas changer.
Les concepteurs de caches s'arrangent pour que des adresses consécutives en mémoire RAM occupent des lignes de cache consécutives, par souci de simplicité. Tout se passe comme suit la mémoire RAM était découpés en blocs de la même taille que le cache. La première adresse du bloc est associée à la première ligne de cache (celle d'indice 0), la seconde adresse est associée à la seconde adresse du_ bloc, et ainsi de suite. Le tout est illustré ci-dessous.
[[File:Cache adressé directement.png|centre|vignette|upright=2|Cache adressé directement.]]
Avec cette contrainte, le circuit de traduction de l'adresse en adresse mémoire pour la RAM interne au cache est drastiquement simplifié, et disparait même. Une partie de l'adresse mémoire sert à indiquer la position de la donnée dans le cache, le reste de l'adresse sert encode le tag et la position de la donnée dans le ligne de cache.
[[File:Cache line.png|centre|vignette|upright=2|Adresse d'une ligne de cache sur un cache adressé directement.]]
Un cache directement adressé est conçu avec une RAM, un comparateur, et un paquet de multiplexeurs. En général, la mémoire RAM stocke les lignes de caches complète. Il arrive que l'on utilise deux mémoires RAM : une pour les tags et une pour les données, mais cette technique augmente le nombre de circuits et de portes logiques nécessaires, ce qui réduit la capacité du cache. L'index à lire/écrire est envoyé sur l'entrée d'adresse de la RAM, la RAM réagit en mettant la ligne de cache sur sa sortie de donnée. Sur cette sortie, un comparateur compare le tag de la ligne de cache lue avec le tag de l'adresse à lire ou écrire. On saura alors si on doit faire face à un défaut de cache. Ensuite, un multiplexeur récupère la donnée à lire/écrire.
[[File:Direct mapped cache - french.png|centre|vignette|upright=2|Cache directement adressé.]]
L'accès à un cache directement adressé a l'avantage d'être très rapide vu qu'il suffit de vérifier une seule ligne de cache : celle prédéfinie. Mais ces caches ne sont cependant pas sans défauts. Vu que le cache est plus petit que la mémoire, certaines adresses mémoires se partagent la même ligne de cache. Si le processeur a besoin d’accéder fréquemment à ces adresses, chaque accès à une adresse supprimera l'autre du cache : tout accès à l'ancienne adresse se soldera par un défaut de cache. Ce genre de défauts de cache causés par le fait que deux adresses mémoires ne peuvent utiliser la même ligne de cache s'appelle un '''défaut par conflit''' (''conflict miss''). Les défauts par conflit n'existent pas sur les caches totalement associatifs. En conséquence, le taux de succès des caches directement adressés est assez faible comparé aux autres caches.
[[File:Cache Block Basic Conflict.svg|centre|vignette|upright=1.5|Exemple de ''Conflict Miss''.]]
===Les caches associatifs par voie===
Les caches associatifs par voie sont un compromis entre les caches directement adressés et les caches totalement associatifs. Pour simplifier, ces caches sont composés de plusieurs caches directement adressés accessibles en parallèle, chaque cache/RAM étant appelé une '''voie'''. Avec ces caches, toute adresse mémoire en RAM est associée à une ligne de cache dans chaque voie.
[[File:Cache associatif par voie.png|centre|vignette|upright=2|Cache associatif par voie.]]
Le schéma ci-dessous compare un cache directement adressé et un cache associatif à deux voies. On voit que chaque adresse est associée à une ligne de cache bien précise avec un cache directement dressé, et à deux lignes de cache avec un cache associatif à deux voies. L'adresse sera associée à 4 lignes de cache sur un cache associatif à 4 voies, à 8 lignes pour un cache à 8 voies, etc. L'ensemble des lignes de cache associées à une adresse est appelé un '''ensemble'''.
[[File:Cache Fill.svg|centre|vignette|upright=2|Comparaison entre un cache directement adressé et un cache associatif à deux voies.]]
Sur ces caches, toute adresse est découpée en trois parties : un tag, un index, et un décalage, comme sur les caches directement adressés. Comme vous pouvez le voir, l'organisation est identique à celle d'un cache totalement associatif, à part que chaque ensemble tag-ligne de cache est remplacé par une mémoire RAM qui en contient plusieurs.
[[File:Implémentation d'un cache associatif par voie.png|centre|vignette|upright=2|Implémentation d'un cache associatif par voie.]]
Le risque de conflits d'accès au cache est donc réduit sur un cache associatif à plusieurs voies, et il est d'autant plus réduit que le cache a de voies. Par contre, leur conception interne fait qu'ils ont un temps d'accès légèrement élevé que les caches directement adressés. Les caches associatifs par voie ont donc un taux de succès et un temps d'accès intermédiaire, situé entre les caches directement adressés et totalement associatifs. Ils sont une sorte de compromis entre réduction des défaut par conflits d'accès au cache et temps d'accès, et complexité des circuits.
==Les optimisations des caches associatifs par voie==
Les caches partiellement associatifs regroupent les caches associatifs par voie et directement adressés, ainsi que leurs variantes. En clair : tous les caches qui ne sont pas totalement associatifs. Ils peuvent être optimisés de nombreuses manières, que ce soit pour gagner en performance ou pour économiser de l’énergie. Dans cette section, nous allons voir quelles sont ces optimisations.
===Les caches pseudo-associatifs===
Les caches adressés par voie contiennent une mémoire SRAM par voie. En théorie, les voies sont accédées en parallèles, en même temps, afin de voir si l'on a un succès de cache ou un défaut. Les '''caches pseudo-associatifs''' sont identiques aux caches associatifs par voie, si ce n'est qu'ils vérifient chaque voie une par une. Ils ont été utilisés sur des processeurs commerciaux, un exemple étant l'IBM 370.
Là encore, on perd en performance pour gagner en consommation d'énergie. Le temps d'accès dans le meilleur des cas est plus faible pour les caches pseudo-associatifs, mais le pire des cas teste tous les caches avant de tomber sur le bon. Les performances sont donc réduites. Mais la consommation énergétique est meilleure, vu qu'on ne vérifie pas forcément toutes les voies en parallèle. On teste la première voie, éventuellement la seconde, peut-être la troisième, etc. Mais dans le cas général, on ne teste qu'une partie des voies, pas toutes, ce qui donne un gain en termes d'énergie.
L'implémentation de caches de ce genre demande que l'on parcoure les voies une par une, en commençant de la première jusqu'à la dernière. Pour cela, un simple compteur suffit. Suivant la valeur du compteur, la voie associée est activée puis accédée. Toute la complexité revient à ajouter un circuit qui prend la valeur du compteur, et active la voie associée, lance un accès mémoire dessus. Vu que les voies sont chacune des caches ''direct mapped'', il suffit pour cela de geler les entrées d'adresse, soit en les déconnectant, soit en utilisant du ''clock gating'' ou de l'évaluation gardée. Les détails d'implémentation, non-cités ici, varient selon le cache.
===La prédiction de voie===
Pour réduire le temps d'accès des caches pseudo-associatifs, certains chercheurs ont inventé la '''prédiction de voie''', qui consiste à faire des paris sur la prochaine voie accédée. L'idée est d'accéder à la voie qui contient la donnée voulue du premier coup, en lisant celle-ci en priorité.
Dans son implémentation la plus simple, le cache reste un cache pseudo-associatif. Lors d'un accès au cache, les voies sont toutes parcoures une par une. Par contre, les voies ne sont donc pas parcourues de la première vers la dernière, mais dans un ordre différent. Cette technique permet de mettre en veille les voies sur lesquels le processeur n'a pas parié, ce qui permet de diminuer la consommation énergétique du processeur. C'est plus efficace que d'aller lire plusieurs données dans des voies différentes et de n'en garder qu'une. L'implémentation est assez simple : il suffit d'ajouter un circuit de prédiction de voie,relié au compteur de voie.
Une amélioration de la technique fait fonctionner le cache comme un intermédiaire entre cache pseudo-associatif et associatif par voies. L'idée est de chercher la voie prédite en premier, puis de chercher dans toutes les voies en parallèle en cas de défaut de cache. Au lieu d'attendre que les comparaisons de tags donnent leur résultat, le processeur sélectionne automatiquement une voie et configure les multiplexeurs à l'avance. Si le processeur ne se trompe pas, le processeur accède à la donnée plus tôt que prévu. S'il se trompe, le processeur annule la lecture effectuée en avance et recommence en faisant un accès en parallèle aux autres voies. Le compromis entre performance et consommation d'énergie est alors différent. On économise de l'énergie par rapport à un cache associatif par voie, au prix d'une petite perte de performance (doublement des temps d'accès). Mais par rapport à un cache pseudo-associatif, l'économie d'énergie est bien moindre, au prix d'un gain en performance assez manifeste.
Prédire quelle voie sera la bonne est assez simple. En vertu du principe de localité, les accès futurs ont des chances de tomber dans les voies les plus fréquemment utilisées ou dans celle plus récemment utilisée. Il suffit de retenir la voie la plus récemment accédée dans un registre, qui sera utilisée comme prédiction. Pour vérifier que la prédiction est correcte, il suffit de comparer le registre et le résultat obtenu après vérification des tags.
Cependant, on peut complexifier l'implémentation pour prendre en compte l'adresse à lire/écrire, l'instruction à l'origine de l'accès mémoire ou tout autre paramètre utile. Par exemple, des instructions différentes ont tendance à aller chercher leurs données dans des ensembles différents et la voie à choisir n'est pas la même. Pour cela, il suffit d'utiliser un cache pour stocker la correspondance instruction - voie. Pour plus de simplicité, la mémoire cache des prédictions est parfois remplacée par une RAM, qui est adressée :
* soit par le program counter de l'instruction à l'origine de l'accès (en réalité, seulement quelques bits de poids faible de l'adresse) ;
* soit par l'adresse à accéder (là encore, quelques bits de poids faible) ;
* soit (pour les modes d'adressage qui utilisent un registre de base et un décalage) par un XOR entre les bits de poids faible de l'adresse de base et le décalage ;
* soit par autre chose.
===La mise en veille sélective des voies===
Les caches associatifs ont tendance à utiliser beaucoup d'énergie, même quand on n'y accède pas. Aussi, certains processeurs détectent quand le cache est peu utilisé et en profitent pour mettre en veille les voies inutilisées. Vous vous demandez certainement ce qui se passe quand une donnée à lire/écrire est dans une voie désactivée. La réponse est que le cache détecte cette situation, car elle déclenche un succès de cache. Les ''tags'' ne sont en effet pas désactivés, seules les données sont mises en veille. L'implémentation est plus simple sur les caches qui séparent les tags et les données dans deux RAM différentes.
Cette optimisation marche surtout sur les gros caches, qui ont des chances d'avoir une portion significative d’inutilisée (pas assez de données pour les remplir), donc généralement les caches L3/L4. Par exemple, les processeurs d'Intel de microarchitecture Ivy Bridge disposent d'un cache de 8 mébioctets à 16 voies, qu'ils peuvent faire passer à 512 kibioctets si le besoin s'en fait sentir. Quand ces processeurs détectent une faible activité, ils mettent en veille 14 voies et n'en gardent que 2 d'actives. Évidemment, les 14 voies sont vidées avant d'être mises en veille, afin qu'une aucune donnée ne soit perdue.
===Les caches ''skew-associative''===
Vous aurez remarqué que dans une voie, les lignes sont accédées en adressage direct : les défauts par conflit sont possibles sur un cache associatif par voie. Pour éviter cela, certains chercheurs ont créé des '''caches ''skew associative''''' (ou associatifs à biais).
Pour faire simple, les index des lignes de cache subissent un petit traitement avant d'être utilisés. Le traitement en question est différent suivant la voie de destination, histoire que deux adresses mémoires avec des index identiques donnent des index différents après traitement. Le traitement en question est souvent une permutation des bits de l'index, qui est différente suivant la voie prise, ou un simple XOR avec un nombre qui dépend de la voie.
[[File:Implémentation d'un cache skew associative.jpg|centre|vignette|upright=2|Implémentation d'un cache skew associative.]]
==L'adressage physique ou logique des caches==
Le cache utilise les adresses à lire/écrire pour déterminer s'il a une copie de la donnée en son sein. Mais l’interaction entre caches et mémoire virtuelle donne lieu à un petit problème : l'adresse utilisée est-elle une adresse virtuelle/logique ou physique ? La réponse varie suivant le processeur : certains caches utilisent l'adresse virtuelle, tandis que d'autres prennent l'adresse physique. On parle de cache '''virtuellement tagué''' dans le premier cas et de cache '''physiquement tagué''' dans le second.
{|
|[[File:Cache tagué virtuellement.png|vignette|Cache tagué virtuellement.]]
|[[File:Cache tagué physiquement.png|vignette|Cache tagué physiquement.]]
|}
===L'accès à un cache physiquement/virtuellement tagué===
La manière d'accéder à un cache dépend de s'il est virtuellement ou physiquement tagué. Il faut utiliser l'adresse virtuelle pour les premiers, physique pour les seconds.
Avec un cache virtuellement tagué, l'adresse logique peut être envoyée directement au cache. La MMU ne traduit les adresses que s'il faut accéder à la mémoire RAM. Ces caches sont donc plus rapides.
Avec un cache physiquement tagué, le processeur doit traduire l'adresse logique en adresse physique dans la MMU, avant d'accéder au cache. La traduction d'adresse se fait soit en accédant à une table des pages en mémoire RAM, soit en accédant à un cache spécifiquement dédié à accélérer la traduction d'adresse, la TLB (''Translation Lookaside Buffer''). Dans la quasi-totalité des cas, la traduction d'adresse passe par la TLB, ce qui fait qu'elle est raisonnablement rapide. Toujours est-il que chaque accès au cache demande d'accéder à la TLB et de faire la traduction d'adresse avant d'accéder au cache. L'accès est donc plus lent que sur les caches virtuellement tagués, où les accès sont plus directs.
[[File:Virtual and Physical addressing.svg|centre|vignette|upright=2|Cache tagué virtuellement versus physiquement tagué.]]
===Les défauts des caches virtuellement tagués===
Les caches physiquement tagués sont moins rapides que les caches virtuellement adressés. Pourtant, les caches virtuellement tagués sont peu fréquents sur les processeurs modernes. Et la raison est assez intéressante : c'est une question d'adresses homonymes et synonymes.
====Les droits d'accès doivent être vérifiés lors d'un accès au cache====
Un premier problème est que la protection mémoire est compliquée avec de tels caches. Rappelons que certaines portions de mémoire sont accessibles seulement en lecture, ou sont interdites en écriture, sont inexécutables, etc. Ces droits d'accès sont gérés par la MMU, qui vérifie pour chaque accès mémoire que l'accès est autorisé. En bypassant la MMU, l'accès au cache virtuellement tagué ne permet pas de faire ces vérifications. Il est possible de charger une donnée en lecture seule dans le cache, mais d'y faire des accès en écriture pour les accès ultérieurs.
Les solutions à cela sont multiples. La première consiste à consulter la MMU en parallèle de l'accès au cache. L'accès au cache est alors réalisé de manière spéculative, et est ensuite confirmé/annulé une fois que la MMU a rendu son verdict. Les performances du cache restent alors les mêmes : l'accès à la MMU se fait en parallèle de l'accès au cache, pas avant. Une autre solution est d'ajouter les droits d'accès en question dans la ligne de cache, dans les bits de contrôle situés après le Tag. Chaque accès au cache récupère ces bits de contrôle et vérifie si l'accès est autorisé. L'inconvénient est que les lignes de cache deviennent plus longues, les droits d'accès sont dupliqués entre MMU et cache. Mais si le budget en transistor suit, ce n'est rien d'insurmontable.
====Les adresses homonymes perturbent la gestion du cache====
Pour rappel, une adresse logique homonyme correspond à plusieurs adresses physiques différentes. Elles surviennent quand chaque programme a son propre espace d'adressage. Dans ce cas, une adresse logique correspondra à une adresse physique différente par programme.Une autre manière de voir les choses est qu'il y a en réalité deux adresses homonymes, qui ont la même valeur, mais appartiennent à des espaces d'adressage différentes. Et c'est cette seconde interprétation que nous allons utiliser.
Les caches doivent gérer ces adresses homonymes et faire en sorte que la lecture/écriture d'une adresse homonyme se fasse à la bonne adresse physique, dans la bonne ligne de cache. Et autant un cache physiquement tagué n'a aucun problème avec ça, vu qu'il ne gère que des adresses physiques, autant des problèmes surviennent avec les caches virtuellement tagués. Le problème est que les caches virtuellement tagués doivent faire la différence entre deux adresses homonymes de même valeur.
Pour corriger ces problèmes, il existe deux grandes méthodes. La première méthode est simple : '''vider les caches''' en changeant de programme. Leur contenu est rapatrié en mémoire RAM, puis les caches sont remis à zéro. Le vidage du cache recopie les lignes de cache ''dirty'' (modifiées) en RAM, puis efface/invalide tout le cache. C'est à cela que servent les instructions ''clean'' et d'invalidation vues plus haut, elles ont été inventées pour cette situation précise. Lorsque le système d'exploitation déclenche une commutation de contexte, à savoir qu'il change le programme en cours d'exécution, le processeur vide tous les caches du processeur. Les interruptions font la même chose, elles vide tous les caches du processeur.
Une seconde méthode numérote chaque programme en cours d'exécution, chaque processus. Le numéro attribué est spécifique à chaque processus, ce qui fait qu'il est appelé un '''identifiant de processus CPU'''. Le processeur mémorise l'identifiant du programme en cours d'exécution dans un registre dédié. L'identifiant de processus CPU est utilisé lors des accès mémoire. Chaque ligne de cache contient le numéro de l'espace d'adressage associé, dans son ''tag''. Lors de chaque accès mémoire, l'ID du registre est comparé à l'ID de la ligne de cache accédée, pour vérifier que l'accès mémoire accède à la bonne donnée. Cette méthode n'est pas très économe en termes de transistors.
L'usage d'identifiant de processus CPU est clairement meilleure en termes de performance, les commutations de contexte sont plus rapides. Par contre, le budget en transistor est plus important. Un autre défaut de cette méthode est que l'identifiant de processus est généralement codé sur une dizaine de bits, alors que le système d'exploitation utilise des identifiants de processus beaucoup plus larges, de 32 à 64 bits sur les CPU 32/64 bits. L'OS doit gérer la correspondance entre identifiants de processus CPU et ceux de l'OS. Parfois, pour cette raison, les OS n'utilisent pas toujours ce système d'identifiant de processus CPU.
====Les adresses synonymes perturbent aussi la gestion du cache====
La gestion des adresses synonymes est aussi un gros problème sur les caches virtuellement tagués. Pour rappel, il s'agit du cas où des adresses logiques différentes pointent vers la même adresse physique. Typiquement, quand deux programmes se partagent un morceau de mémoire, ce morceau correspondra à des adresses synonymes dans les deux espaces d'adressage. Mais il arrive que l'on ait des adresses synonymes dans le même espace d'adressage, ce n'est pas si rare !
Autant les adresses synonymes ne posent aucun problème avec les caches physiquement tagués, ce n'est pas le cas avec les caches virtuellement adressés. Sur ces caches, deux adresses logiques synonymes vont tomber dans deux lignes de cache différentes. Corriger ce problème demande d'ajouter des circuits annexes pour détecter les adresses synonymes, qui sont vraiment complexes et ont un cout en termes de performance. Aussi, les caches virtuellement tagués sont très peu utilisés sur les processeurs modernes.
===Les caches virtuellement adressés, mais physiquement tagués===
Si les caches physiquement et virtuellement tagués ont des défauts, il existe un intermédiaire qui est un bon compromis entre ces deux extrêmes. Il s'agit des '''caches virtuellement adressés - physiquement tagués''', aussi appelés '''caches pseudo-virtuels'''. Pour comprendre comment ils fonctionnent, précisons que ces caches sont soit des caches ''direct-mapped'', soit des caches associatifs par voie (composés de plusieurs RAM ''direct-mapped'' accédées en parallèle, plusieurs voies).
L'accès à ce genre de cache se fait en deux temps : on accède à un ou plusieurs RAM ''direct-mapped'' et on vérifie ensuite les ''Tags'' pour sélectionner la bonne voie. Sur les caches ''direct-mapped'', on n'a qu'une seule RAM ''direct-mapped''. Sur les caches associatifs, on a plusieurs RAM ''direct-mapped'', appelées des voies, qui sont accédées en parallèle. L'accès se fait donc en deux étapes : adresser les RAM ''direct-mapped'' avec un indice, vérifier les ''tags'' avec le reste de l'adresse.
Une autre chose à rappeler est que l'adresse logique est composée de deux parties : un numéro de page logique qui indique dans quel page se situe l'adresse, un décalage/''offset'' qui indique la position de l'adresse dans la page. La traduction d'adresse transforme le numéro de page logique en numéro de page physique, mais laisse le décalage intouché. L'idée est d'utiliser le décalage pour adresser les RAM avec le décalage, tandis que le numéro de page sert de ''tag''. Le décalage est découpé en deux lors de l'accès au cache : les bits de poids fort forment l'indice (l'adresse envoyée à la voie), les bits de poids faible donnent la position de l'adresse dans la ligne de cache.
L'idée est d'utiliser un numéro de page physique pour les ''tags'', mais d'adresser les voies avec le décalage logique. Les deux servent à des instants différents : vérification des ''tags'' pour l'adresse physique, accès aux voies pour l'adresse logique. Ainsi, le problème des adresses synonymes ou homonymes est résolu par l'utilisation de l'adresse physique pour les tags. Par contre, l'accès au cache est plus rapide, car on utilise l'adresse logique pour la première étape. Le processeur accède à la TLB et récupère l'adresse physique pendant que l'on adresse les voies, les deux sont faits en parallèle, ce qui fait que tout se passe comme si l'accès à la TLB était gratuit. La TLB étant assez rapide comparé au cache, l'adresse physique est disponible quand on doit faire la comparaison avec les ''tags''.
[[File:Virtual - Physical - Pseudo Virtual addressing.svg|centre|vignette|upright=2|Adressage pseudo virtuel des caches.]]
Il s'agit d'un excellent compromis entre performance et correction des problèmes des adresses synonymes/homonymes. Tous les caches des processeurs haute performance utilisent cette méthode, au moins pour leurs caches L1. Les caches L2 tendent à utiliser des caches physiquement adressés, pour lesquels la latence d'accès est suffisante pour qu'on accède à la TLB en amont. La raison est assez simple à expliquer, elle provient d'une contrainte assez précise sur le calcul de l'indice.
La conséquence est qu'un cache ''direct-mapped'' ne peut pas dépasser la taille d'une page, soit 4 kibioctets sur les ordinateurs actuels. Sur les caches associatifs, on peut dépasser cette limite en augmentant le nombre de voies, mais la taille maximale d'une voie reste celle d'une page. Cette contrainte n'est pas trop grave sur les caches de petite taille, dont les caches L1. La plupart d'entre eux ont trouvé un compromis idéal avec moins d'une dizaine de voies par cache, chacun de 4 kibioctets, ce qui donne des caches allant de 16 à 64 kibioctets, soit entre 4 et 16 voies. Par contre, un cache de grande taille doit utiliser un grand nombre de voies, ce qui est peu pratique. Aussi, cette technique de caches pseudo-virtuels n'est pas toujours appliquée sur les caches L2, qui sont physiquement adressés. Il faut dire qu'on accède au cache L2 lors d'un défaut dans le cache L1, et l'adresse physique est disponible à ce moment-là, elle a déjà été récupérée lors de l'accès au cache L1. On peut donc l'utiliser pour adresser le cache L2 sans perte de performance.
==Le remplacement des lignes de cache==
Lorsqu'un cache est rempli et qu'on charge une nouvelle donnée dedans, il faut faire de la place pour cette dernière. Dans le cas d'un cache directement adressé, il n'y a rien à faire vu que la ligne de cache à évincer est déterminée lors de la conception du cache. Mais pour les autres caches, la donnée peut aller dans n'importe quelle ligne ou voie. Or, le choix des données à rapatrier en RAM doit être le plus judicieux possible : on doit virer de préférence des données inutiles. Rapatrier une donnée qui sera surement utilisée sous peu est inutile, et il vaudrait mieux supprimer des données qui ne serviront plus ou alors dans longtemps.
Il existe différents algorithmes spécialement dédiés à résoudre ce problème efficacement, directement câblés dans les unités de gestion du cache. Certains sont vraiment très complexes, aussi je vais vous présenter quelques algorithmes particulièrement simples.
Mais avant de voir ces algorithmes, il faut absolument que je vous parle d'une chose très importante. Quel que soit l'algorithme en question, il choisit la ligne de cache à évincer et recopie son contenu dans la RAM. Ce qui demande d'identifier et de sélectionner une ligne de cache parmi toutes les autres. Pour cela, le circuit de remplacement attribue une adresse chaque ligne de cache ! Vous avez bien vu : chaque ligne de cache est numérotée par une adresse, interne au cache.
===Le remplacement aléatoire===
Premier algorithme : la donnée effacée du cache est choisie au hasard ! C'est contre-intuitif, mais cet algorithme donne des résultats assez honorables, en plus d'utiliser très peu de portes logiques (un générateur de nombres pseudo-aléatoire est un circuit assez simple). Généralement, les défauts de cache sont séparés par un nombre assez important et irrégulier de cycles d'horloge. Dans ces conditions, cette technique donne un bon résultat.
===FIFO : first in, first out===
Avec l'algorithme FIFO, la donnée effacée du cache est la plus ancienne, celle chargée dans le cache avant les autres. Cet algorithme est très simple à implémenter en circuit, concevoir une mémoire de type FIFO n'étant pas très compliqué, comme on l’a vu dans le chapitre dédié à ce type de mémoires. Et on peut dire que dans le cas d'un cache, l'implémentation est encore plus simple et se contente d'un seul registre/compteur. Typiquement, il suffit d'ajouter un registre qui mémorise où se situe la donnée la plus récente. Toute insertion d'une nouvelle donnée se fait à l'adresse suivante, ce qui demande juste d'incrémenter le registre avant d'utiliser son contenu pour l'accès mémoire.
[[File:Algorithme FIFO de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme FIFO de remplacement des lignes de cache.]]
Cet algorithme possède une petite particularité sur les caches associatifs par voie : en augmentant le nombre d'ensembles, les performances peuvent se dégrader : c'est ce qu'on appelle l''''anomalie de Bélády'''.
===MRU : most recently used===
Avec l'algorithme MRU, la donnée remplacée est celle qui a été utilisée le plus récemment. Cet algorithme s'implémente simplement avec un registre, dans lequel on place le numéro de la dernière ligne de cache utilisée.
Cet algorithme de remplacement est très utile quand un programme traverse des tableaux du premier élément jusqu'au dernier : les données du tableau sont rarement réutilisées, rendant le cache inutile. Il est prouvé que dans ces conditions, l'algorithme MRU est optimal. Mais dans toutes les autres conditions, cet algorithme a des performances assez misérables.
===LFU : least frequently used===
Avec l'algorithme LFU, la donnée supprimée est celle qui est utilisée le moins fréquemment. Cet algorithme s'implémente en associant un compteur à chaque ligne de cache, qui est incrémenté à chaque accès mémoire. La ligne la moins récemment utilisée est celle dont le compteur associé a la plus petite valeur. Implémenter cet algorithme prend pas mal de transistors, car il faut rajouter autant de compteurs qu'il y a de lignes de cache, en plus d'un circuit pour comparer les compteurs et d'un encodeur.
[[File:Algorithme LFU de remplacement des lignes de cache.png|centre|vignette|upright=2|Algorithme LFU de remplacement des lignes de cache]]
===LRU : least recently used===
Avec l'algorithme LRU, la donnée remplacée est celle qui a été utilisée le moins récemment. Cet algorithme se base sur le principe de localité temporelle, qui stipule qu'une donnée accédée récemment a de fortes chances d'être réutilisée dans un futur proche. Et inversement, la donnée la moins récemment utilisée du cache est celle qui a le plus de chance de ne servir à rien dans le futur. Autant la supprimer en priorité pour faire de la place à des données potentiellement utiles.
Implémenter l'algorithme LRU peut se faire de différentes manières, qui ont pour point commun d'enregistrer les accès au cache pour en déduire la ligne la moins récemment accédée. La manière la plus simple demande d'utiliser un compteur pour chaque ligne de mémoire cache, un peu comme le LFU. La différence avec le LFU est que le compteur n'est pas incrémenté lors d'un accès mémoire. À la place, ce compteur est incrémenté régulièrement, chaque incrémentation ayant lieu en même temps pour tous les compteurs. Quand un bloc est chargé dans le cache, ce compteur est mis à zéro. Quand une ligne de cache doit être remplacée, un circuit va vérifier la valeur de tous les compteurs : la ligne LRU (la moins récemment utilisée), est celle dont le compteur a la valeur la plus haute. Le circuit est composé d'un paquet de comparateurs, et d'un encodeur, comme pour l'agorithme LFU.
===Les approximations du LRU===
Implémenter le LRU demande un nombre de transistors proportionnel au carré du nombre de lignes de cache. Autant dire que le LRU devient impraticable sur de gros caches. Ce qui fait que les processeurs modernes implémentent des variantes du LRU, moins couteuses en transistors, qui donnent un résultat approximativement semblable au LRU. En clair, ils ne sélectionnent pas toujours la ligne de cache la moins récemment utilisée, mais une ligne de cache parmi les moins récemment utilisées. Ce n'est pas un problème si grave que cela car les lignes les moins récemment utilisées ont toutes assez peu de chance d'être utilisées dans le futur. Entre choisir de remplacer une ligne qui a 0,5 % de chances d'être utilisée dans le futur et une autre qui a une chance de seulement 1 %, la différence est négligeable en termes de taux de succès. Mais les gains en termes de circuits ou de temps d'accès au cache de ces algorithmes sont très intéressants.
L'algorithme le plus simple consiste à couper le cache (ou chaque voie s'il est associatif) en plusieurs sections. L'algorithme détermine la section la moins récemment utilisée, avant de choisir aléatoirement une ligne de cache dans cette section. Pour implémenter cet algorithme, il nous suffit d'un registre qui mémorise le morceau le moins récemment utilisé, et d'un circuit qui choisit aléatoirement une ligne de cache. Cette technique s'adapte particulièrement bien avec des caches associatifs à voies : il suffit d'utiliser autant de morceaux que de voies.
Autre algorithme, un peu plus efficace : le '''pseudo-LRU de type M'''. Cet algorithme attribue un bit à chaque ligne de cache, bit qui sert à indiquer de façon approximative si la ligne de cache associée est une candidate pour un remplacement ou non. Il vaut 1 si la ligne n'est pas une candidate pour un remplacement et zéro sinon. Le bit est mis à 1 lorsque la ligne de cache associée est lue ou écrite. Évidemment, au fil du temps, toutes les lignes du cache finiront par avoir leur bit à 1. Lorsque cela arrive, l'algorithme remet tous les bits à zéro, sauf pour la dernière ligne de cache accédée. L'idée derrière cet algorithme est d'encercler la ligne de cache la moins récemment utilisée au fur et à mesure des accès. L'encerclement commence lorsque l'on remet tous les bits associés aux lignes de cache à 0, sauf pour la ligne accédée en dernier. Au fur et à mesure des accès, l'étau se resserre autour de la ligne de cache la moins récemment utilisée. Après un nombre suffisant d'accès, l'algorithme donne une estimation particulièrement fiable. Et comme les remplacements de lignes de cache sont rares comparés aux accès aux lignes, cet algorithme finit par donner une bonne estimation avant qu'on ait besoin d'effectuer un remplacement.
Le dernier algorithme d'approximation, le '''PLURt''', se base sur ce qu'on appelle un arbre de décision. Il a besoin de n − 1 bits pour déterminer la ligne LRU. Ces bits doivent être organisés en arbre, comme illustré plus bas. Chacun de ces bits sert à dire : le LRU est à ma droite ou à ma gauche : il est à gauche si je vaux 0, et à droite si je vaux 1. Trouver le LRU se fait en traversant cet arbre, et en interprétant les bits un par un. Au fur et à mesure des lectures, les bits sont mis à jour dans cet arbre, et pointent plus ou moins bien sur le LRU. La mise à jour des bits s'effectue lors des lectures et écritures : quand une ligne est lue ou écrite, elle n'est pas la ligne LRU. Pour l'indiquer, les bits à 1 qui pointent vers la ligne de cache sont mis à 0 lors de la lecture ou écriture.
{|
|[[File:Organisation des bits avec l'algorithme PLURt.jpg|vignette|Organisation des bits avec l'algorithme PLURt.]]
|[[File:Ligne de cache pointée par les bits de l'algorithme.png|vignette|Ligne de cache pointée par les bits de l'algorithme.]]
|}
===LRU amélioré===
L'algorithme LRU, ainsi que ses variantes approximatives, sont très efficaces tant que le programme respecte relativement bien la localité temporelle. Par contre, Le LRU se comporte assez mal dans les circonstances ou la localité temporelle est mauvaise mais où la localité spatiale est respectée, le cas le plus emblématique étant le parcours d'un tableau. Pour résoudre ce problème, des variantes du LRU existent.
Une variante très connue, l''''algorithme 2Q''', utilise deux caches : un cache FIFO pour les données accédées une seule fois et un second cache LRU. Évidemment, les données lues une seconde fois sont migrées du cache FIFO vers le cache LRU, ce qui n'est pas très pratique. Les processeurs n'utilisent donc pas cette technique, mais celle-ci est utilisée dans les caches de disque dur.
D'autres variantes du LRU combinent plusieurs algorithmes à la fois et vont choisir lequel de ces algorithmes est le plus adapté à la situation. Notre cache pourra ainsi détecter s’il vaut mieux utiliser du MRU, du LRU, ou du LFU suivant la situation.
==Les écritures dans le cache : gestion et optimisations==
Les écritures se font à une adresse mémoire bien précise, qui peut ou non être chargée dans le cache. Si la donnée à écrire est chargée dans le cache, elle est modifiée directement dans le cache, mais elle ne l'est pas forcément en mémoire RAM. Suivant le processeur, les écritures sont ou non propagées en mémoire RAM. Il existe deux stratégies d'écritures, appelées respectivement le ''write-back'' et le ''write-through''.
Avec un cache ''write-back'', si la donnée à mettre à jour est présente dans le cache, on écrit dans celui-ci sans écrire dans la mémoire RAM. Dans ces conditions, une donnée n'est enregistrée en mémoire que si celle-ci quitte le cache, ce qui évite de nombreuses écritures mémoires inutiles.
[[File:Cache write-through.png|centre|vignette|upright=2|Cache write-through.]]
Avec les caches '''Write-Through''', toute écriture dans le cache est propagée en RAM. Cette stratégie augmente le nombre d'écritures dans la mémoire RAM, ce qui peut saturer le bus reliant le processeur à la mémoire. Les performances de ces caches sont donc légèrement moins bonnes que pour les caches ''write back''. Par contre, ils sont utiles dans les architectures avec plusieurs processeurs, comme nous le verrons dans les chapitres sur les architectures multiprocesseurs.
[[File:Cache write-back.png|centre|vignette|upright=2|Cache write-back.]]
===Les caches ''Write-through''===
Sans optimisation particulière, on ne peut écrire dans un cache ''write-through'' pendant qu'une écriture en RAM a lieu en même temps : cela forcerait à effectuer deux écritures simultanées, en comptant celle imposée par l'écriture dans le cache.
Pour éviter cela, certains caches ''write-through'' intègrent un '''tampon d’écriture''', qui sert de file d'attente pour les écritures en RAM. C'est une mémoire FIFO dans laquelle on place temporairement les données à écrire en RAM, où elles attendent en attendant que la RAM soit libre. Grâce à lui, le processeur peut écrire dans un cache même si d'autres écritures sont en attente dans le tampon d'écriture. Par souci d'efficacité, des écritures à la même adresse en attente dans le tampon d’écriture sont fusionnées en une seule. Cela fait un peu de place dans le tampon d’écriture, et lui permet d'accumuler plus d'écritures avant de devoir bloquer le cache. Il est aussi possible de fusionner des écritures à adresses consécutives de la mémoire en une seule écriture en rafales. Dans les deux cas, on parle de '''combinaison d'écriture'''.
Mais la technique du tampon d'écriture a cependant un léger défaut qui se manifeste dans une situation bien précise : quand le processeur veut lire une donnée en attente dans le tampon d’écriture. La première manière de gérer cette situation est de mettre en attente la lecture tant que la donnée n'a pas été écrite en mémoire RAM. On peut aussi lire la donnée directement dans le tampon d'écriture, cette optimisation portant le nom de '''''store-to-load forwading'''''. Dans tous les cas, il faut détecter le cas où une lecture accède à une donnée dans le tampon d'écriture. À chaque lecture, l'adresse à lire est envoyée au tampon d'écriture, qui vérifie si une écriture en attente se fait à cette adresse. Pour cela, le tampon d’écriture doit être un cache, dont chaque entrée mémorise une écriture. Chaque ligne de cache contient la donnée à écrire, et le tag de la ligne de cache contient l'adresse où écrire la donnée. Notons que cache d'écriture a une politique de remplacement de type FIFO, le tampon d'écriture non-optimisé étant une mémoire FIFO.
===Les caches ''Write-back''===
Les caches ''write-back'' ont beau avoir des performances supérieures à celles des caches ''write-through'', il existe des optimisations qui permettent d'améliorer leurs performances. Ces optimisations consistent à ajouter des caches spécialisés à côté du cache proprement dit. Ces caches permettent de mémoriser des données qui sont éliminées du cache par les algorithmes de remplacement de ligne cache, sans pour autant faire une écriture en RAM.
En suivant la procédure habituelle de remplacement des lignes de cache, on doit rapatrier la ligne en RAM avant d'en charger une nouvelle. On peut améliorer la situation en faisant l'inverse : on charge la nouvelle ligne pendant que l'ancienne donnée est rapatriée en RAM. Ainsi, la nouvelle ligne est disponible plus tôt pour le processeur, diminuant son temps d'attente. Pour implémenter cette technique, on doit mémoriser l'ancienne ligne de cache temporairement dans un '''cache d’éviction''' (ou ''write-back buffer'').
[[File:Cache d’éviction.png|centre|vignette|upright=2|Cache d’éviction]]
Les caches directement adressés ou associatifs par voie possèdent aussi un tampon d’écriture amélioré. Pour limiter les défauts par conflit de ces caches, des scientifiques ont eu l'idée d'insérer un cache pour stocker les données virées du cache. En faisant ainsi, si une donnée est virée du cache, on peut alors la retrouver dans ce cache spécialisé. Ce cache s'appelle le '''cache de victime'''. Ce cache de victime est géré par un algorithme de suppression des lignes de cache de type FIFO. Petit détail : ce cache utilise un tag légèrement plus long que celui du cache directement adressé au-dessus de lui. L'index de la ligne de cache doit en effet être contenu dans le tag du cache de victime, pour bien distinguer deux adresses différentes, qui iraient dans la même ligne du cache juste au-dessus.
[[File:Victim Cache Implementation Example.svg|centre|vignette|upright=1|Cache de victime.]]
===La configuration du fonctionnement du cache===
Sur de nombreux processeurs, il est possible de configurer la mémoire cache pour qu'elle fonctionne soit en mode ''write-back'', soit en mode ''write-through''. Pour cela, les processeurs modernes incorporent des '''registres de configuration du cache'''. Le terme ''registre de configuration du cache'' est assez transparent et indique bien quel est leur rôle. Ils configurent comment le cache est utilisé et permettent notamment de configurer le cache pour dire s'il doit fonctionner en mode ''write-back'' ou ''write-through''. Ils permettent aussi d'activer ou de désactiver la combinaison sur écriture.
Les registres en question sont configurés soit par le BIOS, soit par le système d'exploitation. Ce sont des registres protégés, que les applications ne peuvent pas configurer, elles n'en ont pas le droit. Typiquement, ils ne sont accessibles en écriture qu'en mode noyau.
Sur les processeurs x86, les registres de configuration du cache sont appelés des '''''Memory type range registers''''' (''MTRRs''). Les MTRRs sont assez nombreux, et il y a notamment une différence entre mode réel et protégé. Si vous vous souvenez des chapitres sur le mode d'adressage et la mémoire virtuelle, vous vous souvenez que les processeurs x86 incorporent plusieurs modes de fonctionnement. En mode réel, le processeur ne peut adresser qu'un mébioctet de RAM, avec un système de segmentation particulier. En mode protégé, le processeur peut adresser toute la mémoire et la segmentation fonctionne différemment, quand elle n'est pas simplement désactivée.
Les MTRRs sont séparés en deux : ceux pour le mode réel, ceux pour le mode protégé. Les MTRRs fixes sont ceux qui configurent le cache en mode réel, ils étaient utilisés pour gérer l'accès au BIOS, à la mémoire VGA de la carte graphique, et quelques autres accès aux entrées-sorties basiques gérées nativement par le BIOS. Pour le mode protégé, les processeurs au-delà du 386 incorporent des MTRRs variables, qui servent pour les autres entrées-sorties en général, notamment les périphériques PCI, la mémoire vidéo de la carte graphique, et j'en passe.
De nos jours, les registres de configuration du cache sont désuets et cette fonctionnalité est gérée directement par la mémoire virtuelle. La table des pages contient, pour chaque page mémoire, des bits de contrôle qui disent si la page mémoire est cacheable ou non. Le contournement de cache est alors géré par le système de mémoire virtuelle, le cache de TLB et tout ce qui va avec.
===L’allocation sur écriture===
Que faire quand une écriture modifie une donnée qui n'est pas dans le cache ? Doit-on écrire la donnée dans le cache, ou non ? Si la donnée est écrite dans le cache, on dit que le cache fait une '''allocation sur l'écriture''' (ou ''write-allocate''). Certains caches effectuent une telle allocation sur écriture, mais d'autres ne le font pas ou du moins pas systématiquement.
L’allocation sur écriture peut se décliner en deux sous-catégories : le '''chargement à la demande''' et l''''écriture immédiate'''. Dans le premier cas, on charge la donnée à modifier dans le cache, et on la remplace avec la donnée écrite. Dans l'écriture immédiate, l'écriture a lieu directement dans le cache et la donnée à modifier n'est pas chargée dans le cache. Évidemment, seule une portion de la ligne de cache contient la donnée écrite (valide), et le reste contient des données invalides. Le cache doit savoir quelles sont les portions du cache qui sont valides : cela demande d'utiliser un ''sector cache''.
[[File:Write-back with write-allocation.svg|centre|vignette|upright=2|Cache Write-back avec allocation sur écriture.]]
Sans allocation sur écriture, l'écriture est transférée directement aux niveaux de cache inférieurs ou à la mémoire si la donnée à modifier n'est pas dans le cache. Certains caches de ce genre utilisent une petite optimisation : lors de toute écriture, ils supposent que l'écriture donnera un succès de cache. Si c'est le cas, la ligne de cache qui contient la donnée est mise à jour avec la donnée à écrire. Mais si ce n'est pas le cas, la ligne de cache est invalidée, et l'écriture est transférée directement à la mémoire ou aux niveaux de cache inférieurs.
[[File:Write-through with no-write-allocation.svg|centre|vignette|upright=2|Cache Write-through sans allocation sur écriture.]]
===La cohérence des caches===
Il arrive parfois que la mémoire d'un ordinateur soit mise à jour, sans que les modifications soient répercutées dans les mémoires cache. Dans ce cas, le cache contient une donnée périmée. Or, un processeur doit toujours éviter de se retrouver avec une donnée périmée et doit toujours avoir la valeur correcte dans ses caches : cela s'appelle la '''cohérence des caches'''. Il est possible de se retrouver avec des valeurs périmées dans le cache sur les ordinateurs avec plusieurs processeurs, ou si un périphérique écrit en RAM, les modifications ne sont pas répercutées automatiquement dans les mémoires cache.
Pour résoudre ce problème, on peut interdire de charger dans le cache des données stockées dans les zones de la mémoire dédiées aux périphériques. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires cache. Autre solution : utiliser le fait que les périphériques déclenchent une interruption matérielle pour laisser le contrôleur DMA accéder à la mémoire. Dans ce cas, il suffit de vider les caches à chaque interruption matérielle. Le processeur peut le faire automatiquement, ou fournir des instructions pour.
==Le ''cache bypassing'' : contourner le cache==
Dans certaines situations, le cache n'est pas utilisé pour certains accès mémoire. Diverses techniques permettent en effet d'effectuer des accès mémoire qui contournent le cache, qui ne passent pas par le cache. Ils sont utilisés quand l'accès en cache fait que des instructions normales ne fonctionnent pas. Par exemple, de tels accès directs à la RAM sont notamment utilisés pour l'implémentation d'instructions atomiques, une classe d'instructions spécifiques utilisées sur les processeurs multicœurs, dont nous parlerons dans plusieurs chapitres. Mais ils sont aussi utilisés pour l'accès aux périphériques, ce que nous allons voir maintenant.
===Accéder aux périphériques demande de contourner le cache===
Pour rappel, un périphérique (au sens d'entrée-sortie) contient des registres d’interfaçage qui ont une adresse au même titre que les cases mémoire. Un périphérique peut à tout instant modifier ses registres d’interfaçage, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. On aurait encore une fois droit à des problèmes de cohérence des caches. Le problème est géré différemment suivant que l'on utilise un espace d'adressage séparé ou des entrées-sorties mappées en mémoire.
La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache. Cela demande d'adapter le cache et le processeur. L'implémentation exacte dépend de comment sont adressés les périphériques. Pour rappel, il y a deux solutions pour adresser les périphériques : soit les périphériques disposent d'un espace d'adressage séparé de celui de la mémoire, soit il y un espace d'adressage unique partagé entre processeur et mémoire. Les deux cas donnent des solutions différentes.
Avec un espace d'adressage séparé, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre. C'est de loin le cas le plus simple : il suffit de concevoir le processeur pour. Il dispose d'instructions séparées pour les accès aux registres d’interfaçage et à la RAM/ROM, les premières ne passent pas par le cache, les autres si.
Avec des entrées-sorties mappées en mémoire, la même solution est utilisée, mais dans une version un peu différente. Là encore, les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches. Là encore, le processeur doit être prévu pour : on doit pouvoir le configurer de manière à marquer certaines zones de la RAM comme non-cacheable.
Reste qu'il faut marquer des régions de la RAM comme non-cacheable. Pour cela, on améliore les registres de configuration du cache, vus plus haut, afin qu'ils permettent de configurer certaines portions de la RAM pour préciser qu'elles ne doivent pas être mises en cache, qu'il faut activer le contournement de cache pour celles-ci.
===Contourner le cache pour des raisons de performance===
Il arrive que des données avec une faible localité soient chargées dans le cache inutilement. Or, il vaut mieux que ces données transitent directement entre le processeur et la mémoire, sans passer par l'intermédiaire du cache. Pour cela, le processeur peut fournir des instructions d'accès mémoire qui ne passent pas par le cache, à côté d'instructions normales. De telle instructions sont appelées des '''instructions mémoire non-temporelles'''. Non-temporelle, dans le sens : pas de localité temporelle (c.a.d que les données ne seront pas réutilisées plus tard).
Mais il existe aussi des techniques matérielles, où le cache détecte à l'exécution les lectures qui gagnent à contourner le cache. La dernière méthode demande d'identifier les instructions à l'origine des défauts de cache, le processeur accédant directement à la RAM quand une telle instruction est détectée. Si une instruction d'accès mémoire fait trop de défauts de cache, c'est signe qu'elle gagne à contourner le cache. L'idée est de mémoriser, pour chaque instruction d'accès mémoire, un historique de ses défauts de cache. Il existe plusieurs méthodes pour cela, mais toutes demandent d'ajouter de quoi mémoriser l'historique des défauts de cache des instructions. L'historique est mémorisé dans une mémoire appelée la '''table d’historique des défauts de lecture''' (''load miss history table''), qui est souvent un cache.
L'historique en question est, dans sa version la plus simple, un compteur de quelques bits incrémenté à chaque succès de cache et décrémenté à chaque défaut de cache, qui indique si l'instruction a en moyenne fait plus de défauts ou de succès de cache. La table associe le ''program counter'' d'une instruction mémoire à cet historique. À la première exécution d'une instruction d'accès mémoire, une entrée de cette table est réservée pour l'instruction. Lors des accès ultérieurs, le processeur récupérer les informations associées et décide s'il faut contourner le cache ou non.
==La hiérarchie mémoire des caches==
[[File:Cache Hierarchy.png|vignette|Hiérarchie de caches]]
On pourrait croire qu'un seul cache est largement suffisant pour compenser la lenteur de la mémoire. Hélas, les processeurs sont devenus tellement rapides que les caches sont eux-mêmes très lents ! Pour rappel, plus une mémoire peut contenir de données, plus elle est lente. Et les caches ne sont pas épargnés. Si on devait utiliser un seul cache, celui-ci serait très gros et donc trop lent. La situation qu'on cherche à éviter avec la mémoire RAM revient de plus belle.
Même problème, même solution : si on a décidé de diviser la mémoire principale en plusieurs mémoires de taille et de vitesse différentes, on peut bien faire la même chose avec la mémoire cache. Depuis environ une vingtaine d'années, un processeur contient plusieurs caches de capacités très différentes : les caches L1, L2 et parfois un cache L3. Certains de ces caches sont petits, mais très rapides : c'est ceux auxquels on va accéder en priorité. Viennent ensuite d'autres caches, de taille variable, mais plus lents. Les processeurs ont donc une hiérarchie de caches qui se fait de plus en plus complexe avec le temps. Cette hiérarchie est composée de plusieurs niveaux de cache, qui vont des niveaux inférieurs proches de la mémoire RAM à des niveaux supérieurs proches du processeur. Plus on monte vers les niveaux supérieurs, plus les caches sont petits et rapides.
Un accès mémoire dans une hiérarchie de cache fonctionne comme suit : on commence par vérifier si la donnée recherchée est dans le cache le plus rapide, à savoir le cache L1. Si c'est le cas,n on la charge depuis ce cache directement. Si elle n’y est pas, on vérifie si elle est dans le cache de niveau supérieur, le cache L2. Et rebelote ! Si elle n'y est pas, on vérifie le cache du niveau supérieur. Et on répète cette opération, jusqu’à avoir vérifié tous les caches. Si la donnée n'est dans aucun cache, on doit alors aller chercher la donnée en mémoire.
[[File:Hiérarchie de caches.png|centre|vignette|upright=2|Hiérarchie de caches]]
Il y a des différences assez notables entre chaque niveau de cache. Par exemple, les différents niveaux de cache n'ont pas forcément les mêmes politiques de remplacement des lignes de cache. Le cache L1 a généralement une politique de remplacement simple, très rapide, mais peu efficace. De même, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1.
Le cache le plus proche de la mémoire est appelé le '''cache de dernier niveau''', ''Last Level Cache'' en anglais. Il a parfois des caractéristiques totalement différentes des autres caches. Par exemple, sur les processeurs multicoeurs, le cache L3 n'est techniquement pas dans le cœur, mais fait partie d'un ensemble de circuits reliés, comme le contrôleur mémoire ou l'interface mémoire. Il fonctionne à une fréquence différente du processeur, n'a pas la même tension d'alimentation, etc.
===Les caches exclusifs et inclusifs===
Notons que du point de vue de cette vérification, il faut distinguer les caches inclusifs et exclusifs. Avec les caches inclusifs, si une donnée est présente dans un cache, alors elle est présente dans les caches des niveaux inférieurs, ce qui implique l'existence de données en doublon dans plusieurs niveaux de cache. À l'opposé, les caches exclusifs font que toute donnée est présente dans un seul cache, pas les autres. Il existe aussi des caches qui ne sont ni inclusifs, ni exclusifs. Sur ces caches, chaque niveau de cache gère lui-même ses données, sans se préoccuper du contenu des autres caches. Pas besoin de mettre à jour les niveaux de cache antérieurs en cas de mise à jour de son contenu, ou en cas d'éviction d'une ligne de cache. La conception de tels caches est bien plus simple.
Dans les '''caches exclusifs''', le contenu d'un cache n'est pas recopié dans le cache de niveau inférieur. Il n'y a pas de donnée en double et on utilise 100 % de la capacité du cache, ce qui améliore le taux de succès. Par contre, le temps d'accès est un peu plus long. La raison est que si une donnée n'est pas dans le cache L1, on doit vérifier l'intégralité du cache L2, puis du cache L3. De plus, assurer qu'une donnée n'est présente que dans un seul cache nécessite aux différents niveaux de caches de communiquer entre eux pour garantir que l'on a pas de copies en trop d'une ligne de cache, ce qui peut prendre du temps.
[[File:Caches exclusifs.png|centre|vignette|upright=2|Caches exclusifs]]
Dans le cas des '''caches inclusifs''', le contenu d'un cache est recopié dans les caches de niveau inférieur. Par exemple, le cache L1 est recopié dans le cache L2 et éventuellement dans le cache L3. Ce genre de cache a un avantage : le temps d'accès à une donnée est plus faible. La raison est qu'il ne faut pas vérifier tout un cache, mais seulement la partie qui ne contient pas de donnée en doublon. Par exemple, si la donnée voulue n'est pas dans le cache L1, on n'est pas obligé de vérifier la partie du cache L2 qui contient la copie du L1. Ainsi, pas besoin de vérifier certaines portions du cache, ce qui est plus rapide et permet de simplifier les circuits de vérification. En contrepartie, l'inclusion fait que qu'une partie du cache contient des copies inutiles, comme si le cache était plus petit. De plus, maintenir l'inclusion est compliqué et demande des circuits en plus et/ou des échanges de données entre caches.
[[File:Caches inclusifs.png|centre|vignette|upright=2|Caches inclusifs]]
Maintenir l'inclusion demande de respecter des contraintes assez fortes, ce qui ne se fait pas facilement. Premièrement, toute donnée chargée dans un cache doit aussi l'être dans les caches de niveau inférieur. Ensuite, quand une donnée est présente dans un cache, elle doit être maintenue dans les niveaux de cache inférieurs. De plus, toute donnée effacée d'un cache doit être effacée des niveaux de cache supérieurs : si une donnée quitte le cache L2, elle doit être effacée du L1. Ces trois contraintes posent des problèmes si chaque cache décide du remplacement des lignes de cache en utilisant un algorithme comme LRU, LFU, MRU, ou autre, qui utilise l'historique des accès. En effet, dans ce cas, le cache décide de remplacer les lignes de cache selon l'historique des accès, historique qui varie suivant chaque niveau de cache. Par exemple, une donnée rarement utilisée dans le L2 peut parfaitement être très fréquemment utilisée dans le L1 : la donnée sera alors remplacée dans le L2, mais sera maintenue dans le L1. On observe aussi des problèmes quand il existe plusieurs caches à un seul niveau : chaque cache peut remplacer les lignes de cache d'une manière indépendante des autres caches du même niveau, donnant lieu au même type de problème.
Pour maintenir l'inclusion, les caches doivent se transmettre des informations qui permettent de maintenir l'inclusion. Par exemple, les caches de niveaux inférieurs doivent prévenir les niveaux de cache supérieurs quand ils remplacent une ligne de cache. De plus, toute mise à jour dans un cache doit être répercutée dans les niveaux de cache inférieurs et/ou supérieurs. On doit donc transférer des informations de mise à jour entre les différents niveaux de cache. Généralement, le contenu des caches d'instruction n'est pas inclus dans les caches de niveau inférieurs, afin d'éviter que les instructions et les données se marchent sur les pieds.
Enfin, il faut aussi savoir que la taille des lignes de cache n'est pas la même suivant les niveaux de cache. Par exemple, le L2 peut avoir des lignes plus grandes que celles du L1. Dans ce cas, l'inclusion est plus difficile à maintenir, pour des raisons assez techniques.
===Les caches eDRAM, sur la carte mère et autres===
D'ordinaire, les mémoires caches sont intégrées au processeur, à savoir que cache et CPU sont dans le même circuit imprimé. Les caches sont donc fabriqués avec de la SRAM, seule forme de mémoire qu'on peut implémenter dans un circuit intégré. Intégrer tous les caches dans le processeur est une solution et efficace. Mais certains processeurs ont procédé autrement.
[[File:Cache-on-a-stick module.jpg|vignette|Cache-on-a-stick module]]
Des processeurs assez anciens incorporaient un cache L1 dans le processeur, mais plaçaient un cache L2 sur la carte mère. Le cache était clippé sur un connecteur sur la carte mère, un peu comme le sont les barrettes de mémoire. On parlait alors de '''''Cache on a stick''''' (COAST). On aurait pu s'attendre à ce que de tels caches soient en DRAM, vu qu'ils sont placés sur des barrettes de RAM, mais la ressemblance avec la mémoire RAM principale s'arrête là. Le cache était fabriqué en mémoire SRAM, même s'il est en théorie possible de faire de tels caches avec de la DRAM.
Les premiers processeurs avec un cache faisaient ainsi, au début des années 90. Il a été introduiot sur les processeurs Motorola, et a été utilisé sur les IBM PC et les Macintosh de l'époque. Les ordinateurs Macintosh utilisaient de tels caches, pour la pluaprt des modèles. Pour ce qui est des PC, les premiers processeurs x86 faisaient pareil, notamment les processeurs Intel. Le 486, le Pentium et le Pentium 2 utilisaient des ''Cache on a stick''.
L'avantage est que cela permettait de mettre plus de cache, à une époque où les circuits étaient limités en transistors. De plus, cela permettait au consommateur de choisir quelle quantité de cache il voulait, selon ses finances. Il était possible de laisser le processeur fonctionner soit sans mémoire cache, soit avec un cache de 256 Kibioctets, de 512 Kibioctets, etc. Il était possible d'upgrader le cache si besoin.
Pour les CPU Intel, le cache était connecté sur le bus système, au même titre que la mémoire RAM et les entrées-sorties. Il faut dire que les processeurs de l'époque utilisaient un bus système et n'avaient pas de bus mémoire dédié. Mais en théorie, rien n’empêche de connecter le cache sur un bus mémoire dédié. Toujours est-il que les lectures et écritures étaient propagées à la fois dans le cache et la RAM. Les écritures se faisaient dans les deux, systématiquement dans la RAM, mais aussi dans le cache en cas de succès de cache. Les lectures étaient servies soit par le cache en cas de succès de cache, soit par la RAM en cas de défaut de cache. Si le cache répondait en premier, la transaction sur le bus se terminait précocement et l'accès en RAM était abandonné.
[[File:Intel486 Иерархия памяти.png|centre|vignette|upright=2.5|Intel486 : le cache était connecté sur le bus système.]]
À l'inverse, certains processeurs possédaient un cache fabriqué en mémoire DRAM, et plus précisément avec de la mémoire eDRAM. Le cache n'était pas intégré dans le même circuit imprimé que le processeur, mais profitait d'une architecture en ''chiplet''. Pour rappel, cela veut dire que le processeur est en réalité composé de plusieurs circuits intégré séparés, mais interconnectés et soudés sur un même PCB carré. Avec un cache en eDRAM, le cache avait son propre circuit intégré, séparé du circuit intégré du processeur ou du circuit intégré pour le contrôleur mémoire/IO. Un exemple est celui du cache des processeurs Intel de microarchitecture Broadwell, que nous allons voir dans la section suivante.
==Les caches splittés (''phased caches'')==
Dans cette section, nous allons voir les '''caches splittés''' (''phased caches''), dans lequel le cache est accédé en deux étapes consécutives. Il ne s'agit pas des caches pipelinés, que nous verrons dans le chapitre sur les processeurs pipélinés, mais laissons cela à plus tard. Nous allons partir du principe que ce sont des caches ''direct-mapped'', mais l'optimisation peut être utilisée sur un cache associatif par voie.
L'idée est de scinder le cache en deux : une mémoire pour les tags, une autre pour les données de la ligne de cache. Les bits de contrôle peuvent être mis dans l'une ou l'autre SRAM, mais ils sont souvent mis dans la RAM pour les tags. En faisant cela, quelques optimisations deviennent possibles, afin de réduire la consommation énergétique en contrepartie d'une perte de performance. La technique s'implémente différemment pour les caches totalement associatifs et partiellement associatifs.
Les caches totalement associatifs splittés sont ceux formés en combinant un cache associatif avec une CAM et une RAM combinée. On envoie l'adresse à lire/écrire à la mémoire associative, elle répond en envoyant une adresse à la mémoire RAM. L'accès se fait donc en deux temps, avec l'adresse dans la RAM comme intermédiaire. Il est possible de séparer physiquement les deux étapes en insérant un registre entre la CAM et la RAM, ce qui permet aussi de pipeliner l'accès. Mais c'est rarement fait en pratique, car le cout en circuit d'une mémoire CAM est trop important. L'équivalent pour un cache totalement associatif optimisé, sans CAM et RAM séparée, est trop gourmande en interconnexions pour être implémentée. Les caches totalement associatifs splittés sont donc très rares, l'auteur ne connait aucun exemple de processeur avec un tel cache.
Il existe une technique équivalente pour les caches ''direct-mapped'', mais elle demande une certaine modification du cache. Dans les caches ''direct-mapped'' non-splittés, on trouve une mémoire SRAM dont chaque mot mémoire contient une ligne de cache entière, tag inclus. Dans leurs versions splittés, la SRAM est séparée en deux : une pour les tags, une autre pour les données. Précisons qu'il s'agit bien de deux mémoires SRAM adressables. L'adresse à laquelle accéder est envoyée à la SRAM des tags, puis ensuite à la SRAM des données si besoin.
L'idée est d’accéder aux tags pour déterminer s'il y a un succès de cache ou un défaut, et ensuite d'accéder aux données. On n’accède pas aux données en parallèle des tags. Faire cela est évidemment plus lent. En cas de défaut de cache, le temps d'accès est similaire : le tag ne correspond pas, on n'accède pas à la SRAM pour les données. Par contre, vu qu'on n'a pas activé la SRAM pour les données, on économise un peu d'énergie, ce qui réduit la consommation d'énergie. En cas de succès de cache, on accède à la SRAM pour les tags, puis à celle pour les données. Pas d'économie d'énergie à l'horizon, sans compter que le temps d'accès augmente : on accède au cache en deux étapes au lieu de faire les deux accès en parallèle.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Précisons cependant que ce design peut avoir deux avantages en termes de performance. Premièrement, le temps d'accès au cache est légèrement amélioré en cas de défaut de cache. En effet, la SRAM des tags est assez petite, idem pour celle des données. Leur temps d'accès est donc plus faible que pour une grosse SRAM contenant données et tags. Le gain en temps d'accès est donc un avantage, qui ne se manifeste surtout en cas de défaut de cache. Un autre avantage est que l'accès au cache se pipeline plus facilement, ce qui fait qu'on peut effectuer plusieurs accès simultanés au cache. Mais nous verrons cela dans quelques chapitres.
===Le contrôleur de cache 82385 pour les CPU Intel 386===
Il est important de noter que la séparation entre tags et RAM peut être telle que les deux ne sont pas sur la même puce de silicium ! Voire que les deux sont séparés du processeur ! C'était le cas quand les mémoires caches ont été introduites sur les processeurs grand public, notamment sur les premiers processeurs Intel. La miniaturisation n'avait pas avancé au point où placer un cache dans le processeur était possible.
Sur le processeur 386 d'Intel, le cache était un cache splitté, séparé du processeur. Concrètement, le processeur i386 était couplé à un contrôleur de cache Intel 82385 et une mémoire SRAM. Le 82385 contenait les ''tags'' et les bits de contrôle, la SRAM contenait les données, les lignes de cache. Un point important est que les lignes de cache faisaient seulement 32 bits/4 octets, pas plus ! On était loin des lignes de cache actuelles, faisant 64 octets/512 bits. Mais c'était beaucoup plus pratique, vu que le bus système faisait 32 bits de large, idem pour l'interface avec le processeur.
Le schéma ci-dessous montre comment le cache s'intégrait avec le bus système. Pour le bus de commande, le cache servait d'intermédiaire : il recevait les commandes et les filtrait suivant les succès/défauts de cache. En cas de succès de cache, les commandes de lecture n'étaient pas envoyées à la mémoire RAM. Les adresses étaient transmises à la fois au cache et au bus système (avec un registre entre le bus système et le processeur). Le bus de donnée était lui connecté à la mémoire SRAM et au processeur, avec des MUX/DEMUX pour faire le choix de la source des lectures.
[[File:Controleur de cache 82385 pour l'Intel 386.png|centre|vignette|upright=2.5|Contrôleur de cache 82385 pour l'Intel 386]]
Le 82385 surveillait ce qui se passait sur le bus et répondait à la place de la RAM pour certaines lectures. C'était un intermédiaire assez passif, qui se contenait de répondre aux succès et défauts en lecture. Le cache était un cache ''write through'' un peu particulier. En cas de succès de cache pour une écriture, le cache met à jour sa ligne de cache et propage l'écriture en mémoire RAM. Par contre, si une écriture fait un défaut de cache, la donnée n'est pas écrite dans le cache. Le seul moyen pour copier une donnée dans le cache était un défaut pour une lecture.
Le 82385 pouvait commander soit un cache ''direct mapped'', soit associatif à deux voies. Le choix entre les deux était le fait d'une entrée : la mettre à 0 indiquait un cache ''direct mapped'', la mettre à 1 forçait un cache à deux voies. La différence entre les deux est que le 82385 était relié à une mémoire SRAM avec un cache ''direct mapped'', deux SRAM pour deux voies. Pour avoir un cache associatif à deux voies, le 82385 devrait gérer deux signaux ''chip select'' pour activer chaque SRAM/voie suivant les besoins. Il avait précisément quatre signaux CS : deux par SRAM, un pour les lectures, un pour les écritures. Notons que les lignes de cache faisaient 32 bits, ce qui pouvait d'obtenir soit avec une SRAM 32 bits, soit avec deux SRAM 16 bits, soit avec 4 SRAM 8 bits. Le 82385 rajoutait 4 sorties, pour masquer chaque octet dans ces 32 bits, qui sont techniquement des signaux ''Output Enable'' pour 4 SRAM 8 bits.
[[File:Interface entre le 82385 et la SRAM du cache.png|centre|vignette|upright=2|Interface entre le 82385 et la SRAM du cache. Beaucoup d'entrées et de sorties liées au bus d'adresse ne sont pas représentées.]]
Il gérait aussi les accès mémoire non-cacheable, à savoir des accès mémoire qui ne doivent pas être pris en compte par le cache. Il considérait certains accès mémoire comme "à ne pas cacher". Notamment, les accès mémoire à une entrée-sortie ne sont pas cachés. Pour rappel, le processeur utilisait un espace d'adressage séparé pour les entrées-sorties, et utilisait donc un bit IO, qui était utilisé par le 82385 pour savoir si l'accès mémoire doit être caché ou non. Il en est de même pour les accès ayant lieu lors d'une interruption, qui ne passent pas par le cache.
Mais au-delà de cette inhibition automatique du cache, le 82385 avait une entrée NCA (''Non Cacheable Access'') : le cache était "désactivé" quand cette entrée était à 1. C'est un peu une sorte de ''chip select'' pour le 82385, limitée aux accès mémoire. Cette entrée permettait de programmer des intervalles d'adresse auxquels ne pas répondre, en utilisant des circuits de décodage d'adresse adaptés. Il avait aussi une entrée X16, qui permettait d'identifier les accès soit à un composant 16 bits. De tels accès ne doivent pas être mis en cache, sans doute parce que cela ne collait pas avec la taille des lignes de cache (32 bits). Et cette entrée permettait d'inhiber ces accès 16 bits d'agir sur le cache, en utilisant le bit du bus de commande adéquat.
Le 82385 pouvait être intégré dans un système à deux processeurs, voire plus. Pour cela, chaque processeur avait son propre 82385 et sa SRAM rien qu'à lui. Il n'y avait pas de cache partagé entre les deux processeurs. Par contre, les deux caches étaient reliés au même bus système. Pour qu'ils ne se marchent pas sur les pieds, il y avait des circuits d'arbitrage pour gérer l'accès au bus. Un des deux 82385 était mis en mode maitre, l'autre était en mode esclave. Le 82385 maitre pouvait prendre le contrôle du bus, le 82385 esclave devait demander l'autorisation au premier pour accéder au bus système.
Le 82385 gérait une forme limitée de cohérence des caches par invalidation. Dès que le 82385 détectait une prise de contrôle du bus par autre chose que le processeur, il surveillait les adresses transmises sur le bus. En cas de succès de cache, la ligne de cache associée était invalidée. Au-delà de ça, le 82385 avait une entrée FLUSH, qui ordonnait une invalidation totale du cache. Si cette entrée est mise à 1, toutes les lignes de cache sont invalidées. Les ''tags'' sont marqués comme invalides, mais les lignes de cache elles-mêmes ne sont pas touchées.
===L'exemple des processeurs Intel de microarchitecture ''Broadwell''===
Un autre exemple est celui du cache L4 des processeurs Broadwell et de quelques processeurs séparés. Ces processeurs ont une organisation en ''chiplet'' où le processeur incorpore plusieurs puces séparées : une puce pour le processeur proprement dit, une puce nommée ''Crystal Well'' pour le cache L4, et une puce IO pour la communication avec la RAM et la carte mère. Le processeur incorporait un cache L4 de 128 mébioctets, composé de mémoire eDRAM, qui était dispersé entre ''Crystal Well'' et les autres puces. Les données du cache L4 étaient dans ''Crystal Well'', alors que les Tags étaient soit dans le processeur lui-même, soit dans la puce IO !
La puce ''Crystal Well'' était une mémoire DRAM adressable tout ce qu'il y a de plus basique, avec cependant quelques optimisations notables. Par exemple, elle avait deux bus séparés pour l'écriture et la lecture. De plus, elle avait une organisation interne avec 128 banques, contre moins d'une dizaine pour la DDR de l'époque et environ 32 banques pour la DDR5 moderne. Elle contenait aussi quelques circuits pour gérer son rôle de mémoire cache, mais rien en ce qui concerne la gestion des tags eux-mêmes.
Sur les processeurs de microarchitecture ''Broadwell'', les tags étaient placés dans le CPU et précisément dans le cache L3. À chaque accès mémoire au cache L3, les tags du cache L4 étaient consultés en parallèle. De fait, l'accès au cache L4 était assez rapide, malgré le fait que les données étaient dans une puce à part. Ajoutons à cela que le processeur et ''Crystal Well'' n'avaient pas la même finesse de gravure ni la même technologie de fabrication. Les tags étaient implémentés avec de la SRAM contre la DRAM pour les données, ce qui fait que la consultation des tags était plus rapide que l'accès aux données.
Par la suite, dans certains CPU de microarchitecture ''skylake'', les tags ont été déplacés en-dehors du processeur pour finir dans le contrôleur mémoire. En faisant cela, le cache L4 pouvait être utilisé par autre chose que le processeur, et notamment par la carte graphique intégrée au CPU. Avec ''broadwell'', le fait que les tags étaient consultés en cas d'accès au L3 empêchait au GPU intégré de consulter le cache L4. Mais en déplaçant les tags dans le contrôleur mémoire, ce n'est plus le cas vu que la carte graphique a aussi accès au bus mémoire. Par contre, le temps d'accès augmente comparé à la solution précédente. On n'accède pas aux tags du L4 en parallèle du L3 : à la place, il faut consulter les tags du L3, détecter un défaut de cache L3, et ensuite accèder aux tags.
===Les caches RAM-configurables===
Un autre avantage des caches splittés est qu'on peut les modifier pour servir à la fois de mémoire cache, mais aussi de ''local store'', de mémoire RAM de petite taille. Le fonctionnement est assez simple à comprendre. Lors d'un accès au cache, on accède aux tags, puis à la RAM interne au cache. Lors d'un accès au ''local store'', on contourne l'accès au tags et on accède à la RAM interne au cache directement. Il s'agit de la technique du '''cache RAM-configurable''. L'usage de cache RAM-configurable est fréquent sur les cartes graphiques récentes, qui incorporent un ou plusieurs processeurs multicoeurs, dont le cache L1 de données est un cache RAM-configurable.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
===La compression de cache===
Une autre optimisation permise par les ''phased caches'' est l'implémentation de techniques de '''compression de cache''', qui visent à compresser des lignes de cache. L'intérêt est qu'on peut stocker plus de données dans le cache, à capacité égale. L'inconvénient est qu'on doit compresser/décompresser les lignes de cache, ce qui demande un circuit en plus et allonge les temps d'accès. En effet, le temps mis pour compresser/décompresser une ligne de cache s'ajoute au temps d'accès. Aussi, la compression de cache sert surtout pour les caches de bas niveau dans la hiérarchie mémoire, les gros caches aux temps d'accès assez longs.
Une première technique, assez simple à implémenter et peu couteuse en circuit, est celle de la '''compression des lignes de cache nulles'''. Elle compresse uniquement les lignes de cache qui ne contiennent que des zéros. L'idée est qu'on ajoute, dans la mémoire des tags, un bit de contrôle pour chaque ligne de cache appelé le bit ''null''. Il indique si la ligne de cache ne contient que des zéros. Quand on lit une ligne de cache, la mémoire des tags est accédée et on vérifie le bit ''null'' : s'il vaut 1, on n'accède pas à la mémoire cache de données et un multiplexeur envoie un zéro sur le port de lecture. Le bit ''null'' est fixé lors de l'écriture d'une ligne de cache : elle passe dans un comparateur avec zéro relié à la mémoire des tags. La comparaison avec zéro peut se faire en parallèle de l'écriture ou avant (dans ce cas, on n'écrit pas la ligne de cache dans le cache).
Les autres techniques de compression de cache permettent de compresser autre chose que des lignes de cache nulles. L'idée est qu'une ligne de cache physique peut par moment mémoriser plusieurs lignes de caches compressées. Par exemple, prenons un cache dont les lignes de cache font 64 octets. Il est possible de compresser deux lignes de cache pour qu'elles fassent chacune 32 octets, et les stocker dans une seule ligne de cache. Les deux lignes de cache auront des tags différents, mais pointeront sur la même ligne de cache physique. Et cela demande d'utiliser un ''phased cache'' dont la mémoire pour les tags est plus grande que la mémoire pour les données. Il n'y a donc plus une bijection entre tags et ligne de cache, mais une relation surjective. Chose qui n'est possible qu'avec un ''phased cache''. De plus, des bits de contrôles associés à chaque ''tag'' indiquent où se trouvent les lignes de cache compressées dans la ligne de cache : est-ce que c'est les 32 octets de poids fort ou de poids faible ?
[[File:Compression de cache.png|centre|vignette|upright=2|Compression de cache]]
Il ne semble pas que les techniques de compression de cache soient implémentées sur les processeurs modernes. Aucun n'utilise de compression de cache, à ma connaissance. Il faut dire que les techniques connues sont de mauvais compromis : le temps d'accès du cache augmente beaucoup, le cout en circuit pourrait être utilisé pour un cache non-compressé mais plus grand. Et notons que la compression de cache ne marche que si les données peuvent se compresser. Si ce n'est pas le cas, une partie de la mémoire des tags est inutilisée.
Une revue de la littérature académique sur la compression de cache est disponible via ce lien, pour les curieux :
* [https://inria.hal.science/hal-03285041 Understanding Cache Compression, par Carvalho et Seznec].
==Les caches adressés par somme et hashés==
Les caches adressés par somme sont optimisés pour incorporer certains calculs d'adresse directement dans le cache lui-même. Pour rappel, certains modes d'adressage impliquent un calcul d'adresse, qui ajoute une constante à une adresse de base. Généralement, l'adresse de base est l'adresse d'un tableau ou d'une structure, et la constante ajoutée indique la position de la donnée dans le tableau/la structure. Les caches hashés et les caches adressés par somme permettent de faire l'addition directement dans la mémoire cache. Voyons d'abord les caches hashés, avant de passer aux caches adressés par somme.
Sur les '''caches hashés''', l'addition est remplacée par une autre opération, par exemple des opérations bit à bit du style XOR, AND ou OR, etc. Seulement, utiliser des opérations bit à bit pose un problème : il arrive que deux couples Adresse/décalage donnent le même résultat. Par exemple, le couple Adresse/décalage 11101111/0001 donnera la même adresse que le couple 11110000/0000. Dit autrement, deux adresses censées être différentes (après application du décalage) sont en réalité attribuées à la même ligne de cache. Il est toutefois possible de gérer ces situations, mais cela demande des astuces de haute volée pour faire fonctionner la mémoire cache correctement.
Sur les '''caches adressés par somme''', le décodeur est modifié pour se passer de l'addition. Pour comprendre comment, il faut rappeler qu'un décodeur normal est composé de comparateurs, qui vérifient si l'entrée est égale à une constante bien précise. Sur un cache ordinaire, l'addition est faite séparément du décodage des adresses par le cache, dans l'unité de calcul ou dans l'unité de génération d'adresse.
[[File:Non sum adressed cache.png|centre|vignette|upright=2|Cache normal.]]
Mais les caches adressés par somme modifient le décodeur, qui est alors composé de comparateurs qui testent si la somme adresse + décalage est égale à une constante.
[[File:Cache adressé par somme.png|centre|vignette|upright=2|Cache adressé par somme.]]
Chaque circuit du décodeur fait le test suivant, avec K une constante qui dépend du circuit :
: <math>A + B = K</math>
Ce qui est équivalent à faire le test suivant :
: <math>A + B - K = 0</math>
En complément à deux, on a <math>- K = \overline{K} + 1</math>. En injectant dans l'équation précédente, on a :
: <math>A + B + \overline{K} + 1 = 0</math>
En réorganisant les termes, on a :
: <math>A + B + \overline{K} = - 1</math>
Il suffit d'utiliser un additionneur ''carry-save'' pour faire l'addition des trois termes. Rappelons qu'un tel additionneur fournit deux résultats en sortie : une somme calculée sans propager les retenues et les retenues en question. Notons que les retenues sont à décaler d'un cran, vu qu'elles sont censées s'appliquer à la colonne suivante. En notant la somme S et les retenues R, on a:
: <math>S + (R << 1) = - 1 </math>, le décalage d'un cran à gauche étant noté <math><< 1</math>.
Ensuite, -1 est codé avec un nombre dont tous les bits sont à 1 en complément à un/deux.
: <math>S + (R << 1) = 111 \cdots 111111</math>
[[File:Sum + retenue add.png|centre|vignette|upright=2|Sum + retenue add]]
Un simple raisonnement nous permet de savoir si le résultat est bien -1, sans faire l'addition <math>S + (R << 1)</math>. En effet, on ne peut obtenir -1 que si la somme est l'inverse des retenues : un 0 dans le premier nombre correspond à un 1 dans l'autre, et réciproquement. En clair, on doit avoir <math>\overline{S} = R << 1</math>. Pour vérifier cela, il suffit de faire un simple XOR entre la somme et les retenues décalées d'un cran. On a alors :
: <math>S \oplus (R << 1) = 111 \cdots 111111</math>
La comparaison avec -1 se fait avec une porte ET à plusieurs entrées. En effet, la porte donnera un 1 seulement si tous les bits d'entrée sont à 1, ce qui est ce qu'on veut tester.
Au final, l'additionneur pour l'addition adresse + décalage est remplacé par un additionneur carry-save suivi d'une couche de portes XOR et d'un comparateur avec une constante, ce qui économise de circuits et améliore les performances.
[[File:Final circuit of sum addressed cache.png|centre|vignette|upright=2|Cache adressé par somme.]]
En prenant en compte que la constante K est justement une constante, certaines entrées de l'additionneur carry-save sont toujours à 0 ou à 1, ce qui permet quelques simplifications à grand coup d’algèbre de Boole. Chaque additionneur complet qui compose l’additionneur carry-save est remplacée par des demi-additionneurs (ou par un circuit similaire). Autant dire que l'on gagne tout de même un petit peu en rapidité, en supprimant une couche de portes logiques. Le circuit de décodage économise aussi des portes logiques, ce qui est appréciable.
==Les caches à accès uniforme et non-uniforme==
Intuitivement, le temps d'accès au cache est le même pour toutes les lignes de cache. Il s'agit de cache appelés '''caches à accès uniforme''', sous-entendu à temps d'accès uniforme. Mais sur les caches de grande capacité, il arrive souvent que le temps de propagation des signaux varie fortement suivant la ligne de cache à lire. D'ordinaire, on se cale sur la ligne de cache la plus lente pour caler la fréquence d'horloge du cache, même si on pourrait faire mieux. Cependant, les '''caches à accès non uniforme''' ont une latence différente pour chaque ligne d'un même cache. Certaines lignes de cache sont plus rapides que d'autres.
Niveau terminologie, nous allons parler de caches UCA et NUCA : ''Uniform Access Cache'' pour les caches à accès uniforme, ''Non-Uniform Access Cache'' pour les caches à accès non-uniforme.
[[File:Caches UCA et NUCA.png|vignette|Caches UCA et NUCA.]]
Les caches NUCA et UCA sont souvent composés de plusieurs banques séparées, typiquement une par voie. Sur les caches UCA, les banques sont interconnectées avec le processeur de manière à ce que toutes les interconnexions ont la même longueur pour toutes les banques. Typiquement, les banques sont organisées en carré, avec les interconnexions qui partent du centre, avec une disposition en H, illustrée ci-contre
Mais avec les caches NUCA, ce n'est pas le cas. Les interconnexions sont simplifiées et ont des longueurs différentes. Les caches NUCA n'ont pas tous le même genre d'interconnexions, qui dépendent du cache NUCA. En général, les interconnexion forme un réseau avec des sortes de routeurs qui redirigent les données/commandes vers la bonne destination : cache ou processeur. Les banques plus proches du processeur sont accessibles plus rapidement que celles éloignées, même si la différence n'est pas énorme.
Les caches NUCA sont généralement associatifs par voie. Les plus simples utilisent une banque par voie pour le cache, ce qui fait que certaines voies répondent plus vite que les autres. La détection des succès de cache est alors plus rapide si la donnée lue/écrite est dans une voie/banque rapide. En théorie, les défauts de cache demandent de vérifier toutes les banques, et se calent donc sur la pire latence. Mais divers caches se débrouillent pour que ce ne soit pas le cas, soit en vérifiant les banquyes unes par une, soit par un mécanisme de recherche plus complexe.
Les caches NUCA sont surtout utilisés pour les caches L3 et L4, éventuellement les caches L2. Les caches L1 sont systématiquement des caches UCA, car la latence de l'accès au cache L1 est utilisée par le processeur pour décider quand lancer les instructions. Pour simplifier, le processeur peut démarrer en avance une instruction avant qu'une opérande soit lue dans le cache L1, de manière à ce que la donnée arrive en entrée de l'ALU pile en même temps que l'instruction. Une histoire d'exécution dans le désordre et d'émission anticipée des instructions qu'on détaillera dans une bonne dizaine de chapitres. Toujours est-il que tout est plus simple pour le processeur si le cache L1 a un temps d'accès fixe. Par contre, les caches L3 et L4 sont traités en attendant que les données arrivent, le processeur reprend l'exécution des instructions quand les caches L3 et L4 ont terminé de répondre, pas avant.
Avec l'association une banque = une voie, la correspondance ligne de cache → bloc de mémoire qui est statique : on ne peut pas déplacer le contenu d'une ligne de cache dans une autre portion de mémoire plus rapide suivant les besoins. Mais la recherche académique a étudié le cas où la correspondance entre une ligne de cache et une banque varie à l’exécution. Pour nommer cette distinction, on parle de caches S-NUCA (''Static NUCA'') et D-NUCA (''Dynamic NUCA'').
Intuitivement, on s'attend à ce que les caches D-NUCA soient plus performants que les caches S-NUCA. Les lignes de cache les plus utilisées peuvent migrer dans une banque rapide, alors que les lignes de cache moins utilisées vont dans une banque éloignée. Les lignes de cache se répartissent dans le cache dynamiquement dans les banques où elles sont le plus adaptées. Mais paradoxalement, le gain des caches D-NUCA est presque nul, voire insignifiant. La raison est que les caches D-NUCA doivent incorporer un système pour déterminer dans quelle banque se situe la donnée pour détecter les succès/défauts de cache, ainsi qu'un système pour migrer les données entre banques. Et ce système augmente le temps d'accès au cache, réduisant à néant l'intérêt d'un cache D-NUCA. Si on économise quelques microsecondes de temps d'accès en passant d'un cache UCA à un cache S-NUCA, ce n'est pas pour les perdre en passant à un D-NUCA. La majorité des caches D-NUCA sont donc en cours de recherche, mais ne sont pas utilisés en pratique.
==La tolérance aux erreurs des caches==
Une mémoire cache reste avant tout une mémoire RAM, bien que ce soit de la SRAM. Elle n'est pas parfaite et est donc sujette à des erreurs, qui peuvent inverser un bit ou l'effacer. De telles erreurs sont liées à des rayons cosmiques très énergétiques, à des particules alpha produites par le packaging ou le métal deu circuit intégré, peu importe : l'essentiel est qu'ils inversent parfois un bit. Les mémoires modernes savent se protéger contre de telles erreurs, en utilisant trois moyens.
===Les mémoires caches ECC et à bit de parité===
Le premier moyen est l'usage de codes correcteurs d'erreurs, qui ajoutent un ou plusieurs bits à la ligne de cache, dans les bits de contrôle. Les bits ajoutés dépendent de la donnée mémorisée dans le byte, et servent à détecter une erreur, éventuellement à la corriger. Le cas le plus simple ajoute un simple bit de parité pour chaque byte et se contente de détecter les erreurs dans les corriger. Les autres codes ECC permettent eux de corriger des erreurs, mais ils demandent d'ajouter au moins deux bits par byte, ce qui a un cout en circuit plus élevé.
Un simple bit de parité permet de détecter qu'un bit a été inversé, mais ne permet pas de corriger l'erreur. En soi, ce n'est pas un problème. Si une erreur est détectée, on considère que la ligne de cache est invalide. Le cache gère la situation comme un défaut de cache et va chercher la donnée valide en mémoire RAM. Le cout en circuits est donc faible, mais les défauts de cache sont plus nombreux. Les codes ECC sont eux capables de corriger les erreurs, si elles ne modifient pas trop de bits d'un coup. Par contre, ils utilisent deux à trois bits par octet, ce qui a un cout en circuits loin d'être négligeable. Il y a donc un compromis entre défauts de cache et cout en circuits.
La gestion de l'ECC est différente suivant le niveau de cache. Généralement, le cache L1 n'utilise pas l'ECC mais se contente d'un simple bit de parité pour éviter la corruption de ses données. Le cache étant petit, les corruptions de données sont assez rares, et les défauts de cache induits faibles. Il est plus important d'utiliser un code de détection d'erreur simple, rapide, qui ne ralentit pas le cache et n'augmente pas sa latence. Si une ligne de cache est corrompue, il a juste à aller lire la ligne depuis le cache L2, ou un niveau de cache inférieur. Du moins, c'est possible sur le cache en question est un cache inclusif et/ou ''write-through''.
Par contre, le niveau de cache L2 et ceux en-dessous utilisent presque systématiquement une mémoire SRAM ECC. La raison principale étant que ce sont des caches assez gros, pour lesquels la probabilité d'une erreur est assez élevée. Plus une mémoire a de bits et prend de la place, plus il y a une chance élevée qu'un bit s'inverse. Et vu que les caches L2/L3/L4 sont par nature plus lents et plus gros, ils peuvent se permettre le cout en performance lié à l'ECC, idem pour le cout en circuit. Sans compter qu'en cas d'erreur, ils doivent aller lire la ligne de cache originelle en mémoire RAM, ce qui est très lent ! Mieux vaut corriger l'erreur sur place en utilisant l'ECC.
===L'usage du ''memory scrubbing'' sur les caches===
La plupart des erreurs ne changent qu'un seul bit dans un byte, mais le problème est que ces erreurs s'accumulent. Entre deux accès à une ligne de cache, il se peut que plusieurs erreurs se soient accumulées, ce qui dépasse les capacités de correction de l'ECC. Dans ce cas, il existe une solution appelée le ''memory scrubbing'', qui permet de résoudre le problème au prix d'un certain cout en performance.
Pour rappel, l'idée est de vérifier les lignes de caches régulièrement, pour éviter que les erreurs s'accumulent. Par exemple, on peut vérifier chaque ligne de cache toutes les N millisecondes, et corriger une éventuelle erreur lors de cette vérification. En faisant des vérifications régulières, on garantir que les erreurs n'ont pas le temps de s'accumuler, sauf en cas de malchance avec des erreurs très proches dans le temps. Il ne s'agit pas d'un rafraichissement mémoire, car les SRAM ne s'effacent pas), mais ça a un effet similaire.
Et évidemment, le ''memory scrubbing'' a un cout en performance. On peut faire une comparaison avec le rafraichissement mémoire : les rafraichissement réguliers réduisent les performances, car cela fait des accès en plus. Des accès qui sont de plus timés à des instants bien précis qui ne sont pas forcément les plus adéquats. Il est possible qu'un rafraichissement ait lieu en même temps qu'un accès mémoire et le rafraichissement a la priorité, ce qui réduit les performances. La même chose arrive avec les vérifications du ''memory scrubbing''. Malgré tout, la technique a été utilisée sur les caches de certains processeurs commerciaux, dont des processeurs AMD Athlon et Athlon 64. Elle est surtout utilisable sur les caches L2/L3, pour lesquels le cout du pseudo-rafraichissement est acceptable.
==Un exemple de cache : le cache d'instruction==
La grande majorité des processeurs utilise deux caches L1 séparés : un '''cache d'instructions''' dédié aux instructions, et un autre pour les données. Une telle organisation permet de charger une instruction tout en lisant une donnée en même temps. Notons que seul le cache L1 est ainsi séparé entre cache de données et d'instructions.
Le cache d’instruction se situe en théorie entre l'unité de chargement et l'unité de décodage. En effet, ce cache prend en entrée une adresse et fournit une instruction. L'adresse est fournie par le ''program counter'', l'instruction est envoyée dans l'unité de décodage. Le cache se situe donc entre les deux. Le cache de données L1 est connecté au chemin de données, et notamment aux unités de communication avec la mémoire, pas au séquenceur.
[[File:Caches L1 et positions dans le processeur.png|centre|vignette|upright=2.5|Caches L1 et positions dans le processeur]]
Les deux caches sont reliés au processeur par des bus séparés, l'ensemble ressemble à une architecture Harvard, mais où les caches remplacent les mémoires RAM/ROM. Le cache d'instruction prend la place de la mémoire ROM et le cache de données prend la place de la mémoire RAM. Évidemment, il y a des niveaux de caches en dessous des caches de données/instruction, et ceux-ci contiennent à la fois données et instructions, les deux ne sont pas séparées dans des mémoires/caches séparés. Raison pour laquelle l'ensemble est appelé une '''architecture Harvard modifiée'''. Architecture Harvard, car l'accès aux données et instructions se font par des voies séparées pour le processeur, modifiée car la séparation n'est effective que pour le cache L1 et pas les autres niveaux de cache, et encore moins la RAM.
Sur les processeurs modernes, il arrive très souvent que le processeur doive charger une instruction et lire/écrire une donnée en même temps. Et à vrai dire, c'est la règle plus que l'exception. L'usage d'une architecture Harvard modifiée permet cela très facilement : on peut accéder au cache d'instruction via un bus, et au cache de donnée avec l'autre
===Pourquoi scinder le cache L1 en cache d'instruction et de données===
L'usage d'un cache d’instruction séparé du cache de données est à contraster avec l'usage d'un cache L1 multiport unique, capable de mémoriser à la fois instructions et données. Les deux solutions sont possibles ont été utilisées. Les premiers processeurs avaient un cache L1 unique et multiport, mais ce n'est plus le cas sur les processeurs modernes, car les contraintes ne sont pas les mêmes.
Le compromis à faire est celui entre deux petits caches rapides et un gros cache plus lent. Pour rappel, plus un cache est petit, plus il est rapide et chauffe moins. Donc au lieu d'utiliser, par exemple, un gros cache lent de 64 Kibioctets, on utilise deux caches de 32 kibioctets, plus rapides. La capacité totale est la même, mais le temps d'accès plus faible. En termes de temps d'accès, la meilleure solution est celle des deux caches simple port. Mais pour ce qui est de l'économie de circuits, c'est moins évident. Entre deux mémoires simple port et une mémoire multiport, la différence en termes de transistors est ambigüe et dépend de la capacité des caches. La différence est surtout notable pour les gros caches, moins pour les petits caches.
Il faut aussi tenir compte de la capacité effective. Avec deux caches séparés, la répartition de la capacité du cache L1 est fixée une bonne fois pour toutes. Par exemple, avec un cache d'instruction de 32 KB et un cache de données de 32 KB, impossible d'allouer 40 KB aux données et 20 aux instructions. Alors qu'avec un cache L1 unique de 64 KB, on pourrait le faire sans soucis. La répartition se fait naturellement, en fonction de la politique de remplacement du cache et est proche de l'optimal. C'est là un désavantage des caches d'instructions/données séparés : une capacité effective moindre.
Tout cela explique pourquoi le cache L1 est le seul à être ainsi scindé en deux, avec une séparation entre instructions et données : les contraintes au niveau du cache L1 et L2 ne sont pas les mêmes. Pour les caches L1, le temps d'accès est plus important que la capacité, ce qui favorise les caches séparés. Par contre, pour les caches L2/L3/L4, le temps d'accès n'est pas déterminant, alors que la capacité effective et l'économie en circuits sont significatives.
===La connexion des caches L1 avec le cache L2===
Pour les connexions avec le cache L2, tout dépend du processeur. Certains utilisent un cache L2 multiport, qui permet aux deux caches L1 de lire ou écrire dans le cache L2 simultanément.
[[File:Cache d'instructions.png|centre|vignette|upright=1.5|Cache d'instructions.]]
Si le cache L2 ne gère pas les accès simultanés, il n'y a qu'un seul bus relié aux caches L1 et au cache L2. On doit effectuer un arbitrage pour décider quel cache a la priorité, chose qui est réalisé par un circuit d'arbitrage spécialisé.
[[File:Circuit d'arbitrage du cache.png|centre|vignette|upright=1.5|Circuit d'arbitrage du cache.]]
Généralement, les caches d'instructions peuvent se permettre d'être plus petits que les caches de données, car les programmes sont souvent plus petits que les données manipulées. Songez que des programmes de quelques mébioctets peuvent parfois remplir la RAM avec plusieurs gibioctets de données. Lancez votre navigateur internet et ouvrez une page web un peu chargée, pour vous en convaincre !
===Les spécificités du cache d'instruction : lecture seule, bloquant, etc===
Les instructions sont rarement modifiées ou accédées en écritures, contrairement aux données. Et cela permet d'utiliser un cache simplifié pour les instructions. Autant un cache généraliste doit permettre les lectures et écritures depuis le processeur (avec les échanges avec la RAM), autant un cache d'instruction peut se contenter des lectures provenant du CPU et des échanges avec la RAM. Le cache d'instructions est donc très souvent en « lecture seule » : le processeur ne peut pas écrire dedans, mais juste le lire ou charger des instructions dedans.
Un cache d'instruction est donc plus simple qu'un cache pour les données : on peut retirer les circuits en charge de l'écriture (mais on doit laisser un port d'écriture pour charger les instructions dedans). Le gain en circuits permet d'utiliser un cache d'instruction plus gros ou au contraire de laisser de la place pour le cache de données. Le gain en termes de capacité compense alors un peu les inconvénients des caches séparés.
Par contre, cela complique la gestion du code automodifiant, c'est-à-dire des programmes dont certaines instructions vont aller en modifier d'autres, ce qui sert pour faire de l'optimisation ou est utilisé pour compresser ou cacher un programme (les virus informatiques utilisent beaucoup de genre de procédés). Quand le processeur exécute ce genre de code, il ne peut pas écrire dans ce cache L1 d'instructions, mais doit écrire dans le cache L2 ou en RAM, avant de recharger les instructions modifiées dans le cache L1. Cela qui prend du temps et peut parfois donner lieu à des erreurs si le cache L1 n'est pas mis à jour.
Les algorithmes de remplacement des lignes de cache optimaux pour les données ne le sont pas pour les instructions, de même que la taille optimale du cache, la taille des lignes de cache optimale, ou même les algorithmes de préchargement. Par exemple, pour le remplacement des lignes de cache, un simple algorithme LRU est presque optimal pour les instructions, autant il peut donner de mauvaises performances quand on manipule beaucoup de tableaux. Cela justifie d'utiliser des caches spécialisés pour chacune. On peut adapter le cache d'instruction à son contenu, ce qui le rend plus rapide ou plus petit à performance égale.
Les caches d'instructions sont généralement des caches bloquants. Il ne servirait à rien de rendre un cache d'instruction non-bloquant, le cout en circuits ne se traduirait pas par une augmentation significative des performances. À l'opposé, les caches de données sont non-bloquants sur les architectures modernes, pour des raisons de performance. Ce qui rend la séparation assez intéressante, les deux caches ayant des besoins différents et des implémentations différentes, cela permet d'optimiser le cout en transistors des caches.
===L'impact du cache d'instruction sur les performances===
Sur les architectures conventionnelles, le cache d'instruction a plus d'impact sur les performances que le cache de données. La raison principale est que les instructions ont une meilleure localité spatiale et temporelle que pour les données. Pour la localité spatiale, les instructions consécutives se suivent en mémoire, alors que rien ne garantit que des données utilisées ensemble soient regroupées en mémoire. Pour localité temporelle, elle est très variable pour les données, mais très courante pour les instructions du fait de l'usage fréquent des boucles et des fonctions.
: La présence de branchements atténue la localité temporelle des instruction, sauf que la majorité des branchements sautent à un endroit très proche, seuls les appels de fonction brisent la localité spatiale.
La conséquence est qu'il arrive que certains CPU aient un cache L1 d'instruction plus gros que celui pour les données. On parle alors de '''cache L1 asymétriques'''. Un exemple est celui des processeurs AMD de microarchitecture Zen, dont le cache d'instruction était deux fois plus gros que le cache de données. Leur cache d'instruction faisait 64 kibioctets, contre seulement 32 pour le cache de données.
D'ailleurs, il existe des processeurs assez extrêmes qui se contentent d'un cache d'instruction unique, sans cache de données. C'est le cas sur les processeurs vectoriels ou les GPU que nous verrons dans les chapitres de fin de ce wikilivres. De tels processeurs sont spécialisés dans la manipulation de tableaux de données, traitement qui a une faible localité temporelle. En conséquence, utiliser un cache de données n'est pas vraiment utile, voire peu être contreproductif, alors qu'un cache d’instruction fonctionne parfaitement.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Compléments sur les mémoires de masse
| prevText=Compléments sur les mémoires de masse
| next=Le préchargement
| nextText=Le préchargement
}}
</noinclude>
h5yh6ty6q9i25phguwuf5i5e8lj7wgb
Mathc initiation/a582
0
81064
772413
772408
2026-09-19T14:50:02Z
Xhungab
23827
772413
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a584#* Les suites usuelles :| Sommaire]]
{{Partie{{{type|}}}|Transformées en Z usuelles, des signaux causales discrets :}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal : ''' '''Transformée en Z : ''' Domaine de convergence
'''f(n)''' '''F(z)'''
'''δ(n) = 1''' '''1''' C '''* L'impulsion unitaire'''
'''u(n) = 1''' '''[[Mathc initiation/0070#L'échelon unitaire : u(n) = 1|z/(z-1)]]''' |z| > 1 '''* L'échelon unitaire'''
'''r(n) = n''' '''[[Mathc initiation/0070#La rampe : r(n) = n|z/(z-1)^2]]''' |z| > 1 '''* La rampe'''
'''c(n) = n^2''' '''z(z+1)/(z-1)^3''' |z| > 1 '''* Le carré'''
'''f(n) = a^n''' '''[[Mathc initiation/0070#L'exponentiel : f(n) = a^n|z/(z-a)]]''' |z| >|a| '''* L'exponentiel'''
'''g(n) = cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' |z| > 1 '''* La fonction cosinus discret'''
'''h(n) = sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' |z| > 1 '''* La fonction sinus discret'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
pl1b7aehtcei77batus8zx7g0f8ioom
772487
772413
2026-09-19T22:05:07Z
Xhungab
23827
772487
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a584#* Les suites usuelles :| Sommaire]]
{{Partie{{{type|}}}|Transformées en Z usuelles, des signaux causales discrets :}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal : ''' '''Transformée en Z : ''' Domaine de convergence
'''f(n)''' '''F(z)'''
'''δ(n) = 1''' '''1''' C '''* L'impulsion unitaire'''
'''u(n) = 1''' '''[[Mathc initiation/0070#L'échelon unitaire : u(n) = 1|z/(z-1)]]''' |z| > 1 '''* L'échelon unitaire'''
'''r(n) = n''' '''[[Mathc initiation/0070#La rampe : r(n) = n|z/(z-1)^2]]''' |z| > 1 '''* La rampe'''
'''c(n) = n^2''' '''[[Mathc initiation/0070#Le carré : c(n) = n^2|z(z+1)/(z-1)^3]]''' |z| > 1 '''* Le carré'''
'''f(n) = a^n''' '''[[Mathc initiation/0070#L'exponentiel : f(n) = a^n|z/(z-a)]]''' |z| >|a| '''* L'exponentiel'''
'''g(n) = cos(kn)''' '''[z^2-z cos(k)]/[z^2-2z cos(k)+1]''' |z| > 1 '''* La fonction cosinus discret'''
'''h(n) = sin(kn)''' '''[z sin(k)]/[z^2-2z cos(k)+1]''' |z| > 1 '''* La fonction sinus discret'''
'''Un signal discret causal est une suite de valeurs x[n] qui est entièrement nulle pour tous les instants de temps négatifs (n < 0)'''
{{AutoCat}}
k3obpuj7hhyt5m6fv0ntu7kxyk9ai96
Mathc initiation/a583
0
81065
772489
772141
2026-09-19T22:16:13Z
Xhungab
23827
772489
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a584#* Les suites usuelles :| Sommaire]]
{{Partie{{{type|}}}|Propriétés des transformées en Z :}}
Soit f(n) u(n) un signal causal discret :
'''* La transformée en Z est un opérateur linéaire :'''
'''Z[a f(n) + b g(n)] = a Z[f(n)] + b Z[g(n)] = a F(z) + b G(z)'''
'''* Multiplication par une exponentielle : [[Mathc initiation/a585|Exemples]] '''
a^n f(n) u(n)
Si Z[f(n)] = F(z) alors '''Z[a^n f(n)] = F(z/a)'''
'''* Multiplication par la variable d'évolution : [[Mathc initiation/a588|Exemples]] '''
n f(n) u(n)
Si Z[f(n)] = F(z) alors '''Z[n f(n)] = -z F'(z)'''
'''* Le retard de no unités : [[Mathc initiation/a589|Exemples]] '''
f(n-no) u(n-no) est un signal causal retardé de no unités.
Image (L'invité retardé de no minutes, arrivera à 12h+no minutes)
Si Z[f(n)] = F(z) alors '''Z[f(n-no)] = z^(-no) F(z)'''
'''* L'avance d'une unité : [[Mathc initiation/a590|Exemples]] '''
f(n+1) u(n+1) est un signal causal avancé de 1 unité.
Image (L'invité en avance de 1 minute, arrivera à 12h-1 minute, 11h59 minutes)
Si Z[f(n)] = F(z) alors '''Z[f(n+1)] = z^(1) [F(z)-f(0)]'''
'''* L'avance de deux unités : [[Mathc initiation/a591|Exemples]]'''
f(n+2) u(n+2) est un signal causal avancé de 2 unité.
Image (L'invité en avance de 2 minutes, arrivera à 12h-2 minutes, 11h58 minutes)
Si Z[f(n)] = F(z) alors '''Z[f(n+2)] = z^(2) [F(z)-f(0)-f(1)z^(-1)]'''
:
{{AutoCat}}
sazo24vawld77b0ct0orslhwez1xb12
Mathc initiation/a588
0
81071
772488
772386
2026-09-19T22:06:12Z
Xhungab
23827
772488
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
:
[[Mathc initiation/a583| Sommaire]]
:
----{{Partie{{{type|}}}|Multiplication par la variable d'évolution : n f(n)|fond={{{fond|}}<nowiki>}</nowiki>}}
Pour simplifier l'écriture j'ai écrit le signal f(n)
au lieu du '''signal causale discret f(n) u(n)'''.
'''Le signal''' '''Transformée en Z'''
'''f(n)''' '''F(z)''' '''Z[n f(n)] = -z F'(z)'''
'''u(n)''' '''z/(z-1)''' '''-z [z/(z-1) ]' ''' = '''-z [-1/(z-1)^2]'''
'''n''' '''z/(z-1)^2''' '''-z [z/(z-1)^2]' ''' = '''-z [-(z+1)/(z-1)^3]'''
'''n^2''' '''z(z+1)/(z-1)^3''' '''-z [z(z+1)/(z-1)^3]' ''' = '''-z [-(z^2+4z+1)/(z-1)^4]'''
'''a^n''' '''z/(z-a)''' '''-z [z/(z-a) ]' ''' = '''-z [-a/(z-a)^2]'''
'''Le signal''' : '''cos(kn)'''
'''Transformée en Z''' : ''' [z^2-z cos(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z^2-z cos(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [2z-(z^2+1)cos(k)]/[z^2-2z cos(k)+1]^2'''
'''Le signal''' : '''sin(kn)'''
'''Transformée en Z''' : ''' [z sin(k)]/[z^2-2z cos(k)+1]'''
'''Z[n f(n)]''' : '''-z [[z sin(k)]/[z^2-2z cos(k)+1]]' '''
'''-z F'(z)''' : '''-z [-[(z^2-1)sin(k)]/[z^2-2z cos(k)+1]^2]'''
{{AutoCat}}
cmwaeue9xyc7yum6xifnsnhgqfsoha1
Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86
0
82608
772452
762413
2026-09-19T18:18:57Z
Mewtow
31375
772452
wikitext
text/x-wiki
Dans ce chapitre, nous allons étudier des exemples de processeurs x86, ceux présents dans nos PC. Nous n'allons pas voir les anciens processeurs comme le 286, le 386 ou le 486. Nous allons commencer avec le Pentium 1, et les processeurs commerciaux qui ont suivis. Tous les processeurs que nous allons voir dans ce chapitre sont des processeurs superscalaires. De fait, ce n'est pas pour rien si ce chapitre se situe après le chapitre sur les processeurs superscalaires. Par contre, nous allons voir que certains n'ont pas d'exécution dans le désordre. La raison est que l'exécution dans le désordre est arrivé après la superscalarité.
==Généralités sur les CPU x86 superscalaires==
Avant de voir chaque processeur indépendamment des autres, nous allons devoir aborder quelques généralités. Nous allons d'abord voir que le jeu d'instruction x86 pose quelques problèmes pour la superscalarité. Et ces problèmes posent des contraintes assez fortes, avec lesquelles les concepteurs de processeurs dovient faire avec. Nous poursuivrons ensuite par un historique des processeurs Intel et AMD, histoire de donner un peu de contexte aux processeurs que nous allons étudier.
===Le jeu d'instruction x86 pose des problèmes pour la superscalarité===
Une difficulté de l'architecture x86 est qu'il s'agit d'une architecture CISC, avec tous les défauts que ça implique. Un jeu d'instruction CISC a en effet de nombreuses propriétés qui collent mal avec l'émission multiple, avec la '''superscalarité'''. Il y en a plusieurs, certaines impactent le chargement des instructions, d'autres leur décodage, d'autres l'exécution, etc.
Premièrement, les instructions sont de longueur variable, entre 1 et 15 octets, ce qui complique leur chargement et leur décodage. En pratique, les processeurs chargent un bloc de 32 à 64 octets, et découpent celui-ci en plusieurs instructions. La conséquence est que l'usage d'instructions trop longues peut poser problème. Imaginez qu'un processeur charge un bloc de 16 octets et que celui-ci ne contienne qu'une seule instruction : on ne profite pas de la superscalarité.
Deuxièmement, une partie des instructions est microcodée, faute de mieux. Et cela pose de sérieux challenges pour l'implémentation des décodeurs. Dupliquer le microcode demanderait trop de transistors, ce qui fait que ce n'est pas fait. À la place, il n'y a qu'un seul microcode, ce qui fait que l'on ne peut pas décoder plusieurs instructions microcodées en même temps. Il est cependant possible de profiter de la superscalarité, en décodant une instruction microcodée en parallèle d'autres instructions non-microcodées. Et heureusement, ce cas est de loin le plus fréquent, il est rare que plusieurs instructions microcodées se suivent.
Troisièmement, la présence d'instructions ''load-up'', qui lisent un opérande en mémoire, peut poser problème, mais est aussi source d'optimisations assez intéressantes. En théorie, une instruction ''load-op'' est décodée en deux micro-opération : une pour lire d'opérande en RAM, l'autre pour faire l'opération arithmétique. Sauf que les processeurs x86 modernes optimisent la gestion des instructions ''load-up''. Par exemple, les premiers processeurs Atom géraient des micro-opérations de type ''load-up'', directement dans le chemin de données !
D'autres processeurs utilisent la technique de la '''micro-fusion''' pour retarder le décodage réel des instructions ''load-up'' assez loin dans le pipeline. Avec eux, une instruction ''load-op'' est décodée en une seule "macro-opération", qui est propagée dans le pipeline, pour être scindée en deux micro-opérations un peu avant les ALU et l'unité mémoire. L'avantage est qu'une macro-opération ne prend qu'une seule entrée dans le tampon de ré-ordonnancement, la fenêtre d'instruction, la file de micro-opération, et les autres structures similaires.
===Un petit historique des processeurs x86 superscalaires===
Nous allons voir les processeurs Intel à part des processeurs AMD. La raison à cela est que les architectures Intel et AMD ont progressivement évolué, chacune se basant sur la précédente et l'améliorant. Il n'y a pas eu de cassure entre microarchitectures AMD, qui sont chacune la suite de la précédente. Il est donc préférable de voir les architectures AMD dans l'ordre chronologique. Par contre, Intel a eu une gigantesque cassure, avec le processeur Pentium 4. Son architecture se démarquait fortement du Pentium 3, mais elle n'a pas convaincu et a été abandonnée avec les processeurs suivants. Ce qui fait nous verrons l'architecture du Pentium 4 à part.
Le Pentium 1 et 2 utilisaient la même architecture, qu'on détaillera dans ce qui suit. Les seules différences importantes étaient la fréquence, le cache, et quelques détails dans le genre. Le Pentium 3 était une nouvelle microarchitecture qui ajoutait l'exécution dans le désordre. Un an et demi plus tard, le Pentium 4 est sorti et a été un échec. Ses performances étaient peu convaincantes face au Pentium 3, et sa consommation énergétique était très importante. La conséquence est que le Pentium 4 et le Pentium 3 ont survécu pendant un long moment, beaucoup de monde préférait acheter un Pentium 3.
Intel a alors amélioré les microarchitectures du Pentium 3 et du 4, indépendamment, pendant environ 7 ans. La microarchitecture du Pentium 3 a subit plusieurs micro-évolutions, chacune avec une finesse de gravure différente, afin de satisfaire les consomateurs. Les premeirs Pentium 3 avaient une finesse de gravure de 250 nm, elle a chuté à 65 sur les derniers modèles.
L'architecture du Pentium 4 a fait la même chose, pour tenter de corriger ses problèmes de performance et de consommation d'énergie. Les premiers Pentium 4 avaient uen finesse de gravure de 180 nm, elle a elle aussi chutée à 65 sur les derniers modèles.
Après l'échec du Pentium 4, les ingénieurs d'Intel ont repris l'architecture P6 et l'ont améliorée fortement, pour donner l'architecture Core. Les micro-processeurs suivants ont fait évoluer cette architecture progressivement, au point où elle ne ressemble plus à l'originale. L'architecture Core a laissé la place à l'architecture Nehalem, puis Sandy Bridge, puis Haswell, puis Skylake, puis Ice Lake, et Golden Cove. Il s'agit de la lignée principale, partant du Pentium 3 et continuant jusqu'à nos jours. Ces microarchitectures ont suivi un motif assez simple, appelé modèle '''tick-tock'''. Chaque microarchitecture était déclinée en deux versions, la seconde ayant une finesse de gravure réduite.
En parallèle, Intel a travaillé sur des processeurs basse performance et basse consommation, avec une microarchitecture très différente. Les processeurs Atom de microarchitecture Bonnel, pour être ensuite remplacés par les microarchitectures Silvermont, puis Goldmont et Gracemont. Ces microarchitectures ont évolué en parallèle de la lignée principale, il s'agit d'une lignée secondaire. Le tout est résumé dans ce schéma ci-dessous.
[[File:IntelProcessorRoadmap-4v.svg|centre|vignette|upright=2.5|Roadmap des processeurs Intel, qui servira de structure pour la suite du chapitre]]
De nos jours, les processeurs Intel utilisent les deux types de micro-architectures en même temps. Les CPU Intel modernes disposent de deux types de coeurs : les coeurs P et le coeurs E. Leur nom signifie "Performance" et "Efficient", qui trahissent leur but. Les coeurs P utilisent la microarchitecture de la lignée principale, qui est conçue pour la performance. Les coeurs E, quant à eux, utilisent les micro-architectures basse consommation de la lignée secondaire.
==Les processeurs x86 d'Intel, la lignée principale==
Pour commencer, nous allons voir les processeurs Intel. N'y voyez pas du favoritisme derrière ce choix, la justification est toute autre. Si je commence par Intel, c'est pour commencer avec les Pentium 1 et 2, qui sont plus simples que tous les autres. Il s'agit en effet de processeurs superscalaires, mais sans exécution dans le désordre. L’absence d'exécution dans le désordre les rend bien plus simples à étudier que les autres, ce qui en fait un bon point de départ pour ce chapitre.
Le successeur du Pentium 2 a intégré l'exécution dans le désordre, ce qui fait que les Pentium 1 et 2 sont les seuls processeurs superscalaires ''in-order''. Pour la concurrence, AMD n'a pas produit de processeurs ''in-order'', tous les processeurs produits par AMD intègrent l'exécution dans le désordre. A la rigueur, il y a bien les processeurs Atom de la lignée secondaire d'Intel. Cependant, même s'ils sont bien des CPU ''in-order'', leur architecture est assez compliquée. En comparaison, le Pentium est une vieille architecture, qui se débrouillait avec peu de transistors et était donc bien plus simple que celle de l'Atom.
===Le Pentium 1/MMX et les pipelines U/V===
Le processeur Pentium d'Intel avait un pipeline de 5 étages : un étage de chargement/prédiction de branchement, deux étages de décodage, un étage d'exécution et un dernier étage pour l'écriture dans les registres. Le Pentium 1 était un processeur double émission, intégrant deux pipelines nommés U et V. Chose importante, les deux pipelines n'étaient pas identiques. Le pipeline U pouvait exécuter toutes les instructions, mais le pipeline V était beaucoup plus limité. Par exemple, seul le pipeline U peut faire des calculs flottants, le pipeline V ne fait que des calculs entiers et des branchements.
Les deux pipelines disposaient d'une unité de calcul entière, identique dans les deux pipelines. Mais le pipeline U incorporait un circuit multiplieur/diviseur et d'un ''barrel shifter''. L'unité flottante était sur le port d'émission du pipeline U, idem pour l'unité de calcul vectoriel MMX sur le Pentium MMX. Les deux pipelines avaient chacun une unité de calcul d'adresse, mais ils n'étaient pas identiques : celle du pipeline V ne gérait que l’instruction LEA, celle du pipeline U gérait tous les calculs d'adresse.
{|class="wikitable"
|-
! Pipeline U
! Pipeline V
|-
| ALU entière
| ALU entière
|-
| Multiplieur/diviseur
|
|-
| ''Barrel Shifter''
|
|-
| AGU complexe
| AGU simple (opération LEA)
|-
| FPU
|
|-
| Unité SIMD
|
|}
Les deux pipelines géraient les opérations bit à bit, les additions, les soustractions et les comparaisons. Les autres instructions ne sont exécutables que dans le pipeline U. Pour être plus précis, les deux pipelines supportaient les instructions suivantes, ce qui fait qu'on pouvait en faire deux en même temps :
* Les instructions arithmétiques INC, DEC, ADD, SUB ;
* l'instruction de comparaison CMP ;
* les instructions bit à bit AND, OR, XOR ;
* l'instruction de calcul d'adresse LEA ;
* l'instruction MOV (dépend du mode d'adressage) ;
* les instructions de gestion de la pile PUSH et POP (dépend du mode d'adressage) ;
* l'instruction NOP, qui ne fait rien.
Il faut noter qu'il y a cependant quelques restrictions, beaucoup de paires d'instructions sont interdites. La plupart interdisent au pipeline V de faire quoique ce soit quand une opération particulière est émise dans le pipeline U. Par exemple, si le pipeline U exécute une multiplication ou une division, le processeur ne peut pas exécuter une opération dans le pipeline V. Et c'est pareil avec les branchements : si un branchement est émis dans le pipeline U, l'instruction suivant le branchement n'est pas émise dans le pipeline V, pour éliminer les dépendances de contrôle. De même, si le pipeline U exécute une opération flottante, le pipeline V ne pourra rien exécuter. La seule exception est l'instruction FCXH, qui échange deux registres flottants.
[[File:Intel Pentium arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel Pentium MMX. On voit que certaines unités de calcul sont dupliquées.]]
Un choix assez intéressant a été fait pour le cache de données. Nous avions vu dans le chapitre sur les CPU superscalaires que la superscalarité a un impact sur l'unité mémoire. Il y a alors deux implémentations. La première ne fait rien, l'unité mémoire ne change pas, et le processeur ne peut pas faire deux accès mémoire simultanés. La seconde duplique l'unité mémoire et les ports de lecture/écriture du cache, ce qui autorise des accès mémoire simultanés. Le Pentium 1 utilise une solution intermédiaire.
Les ingénieurs d'Intel étaient partis à la base sur un cache totalement double port, pour obtenir des performances maximales. Mais diverses simulations et observations les ont fait changer d'avis. Les simulations ont montré qu'il est "rare" que les deux pipelines aient besoin de lire/écrire dans le cache en même temps. Et ils ont optimisé le cache de donnée pour en tenir compte. Le cache de données est partiellement multiport : simple port sur certains aspects, double port sur d'autres.
Le cache est un cache splité, à savoir que les données et les ''tags'' sont séparés dans des mémoires séparées. La mémoire pour les ''tags'' est multiport, ce qui permet d'interroger les ''tags'' du cache deux fois par cycle. Un port est relié au pipeline u, un autre au pipeline V. Mais pour les données, le cache n'a qu'un seul port pour lire/écrire des données. Impossible donc de lire deux données en même temps, pour alimenter les deux pipelines. Il utilise cependant 8 banques permet d'accélérer des accès mémoire proches dans le temps, mais dans des cycles d'horloge différents.
Entre les deux mémoires, il y a un circuit qui détecte les conflits, à savoir les situations où les deux pipelines accèdent en même temps au cache. S'ils veulent lire/écrire une donnée dans le même cycle, ce qui est impossible avec un seul port, le pipeline U a la priorité et le pipeline V attend le cycle suivant. Le circuit détecte aussi les conflits de banque, à savoir quand un pipeline accède à une banque en cours d'accès par l'autre pipeline (on rappelle qu'un accès au cache prend plusieurs cycles). Le circuit détecte aussi certaines dépendances mémoires, à savoir des accès consécutifs à la même adresse.
: La TLB du processeur est aussi totalement double port.
===La microarchitecture P6 du Pentium 2/3===
Le Pentium 3 utilisait la '''microarchitecture P6''', qui a été dérivée dans de nombreuses variantes, dont les finesses de gravure n'étaient pas les mêmes. Il introduit une exécution dans le désordre simple, avec une fenêtre d'instruction centralisée, avec renommage dans le désordre dans le ROB (tampon de ré-ordonnancement), commandé par une table d'alias. C'était un processeur triple émission, soit une instruction de plus que la double émission du Pentium 1.
Le pipeline passe de 5 étage sur le Pentium à 14 - 12 étages, dont le détail est le suivant :
* Prédiction de branchement, deux cycles ;
* Chargement des instructions, trois cycles ;
* Décodage de l'instruction, deux cycles ;
* Renommage de registre, un cycle ;
* Copie des opérandes dans le tampon de ré-ordonnancement (lié au renommage de registre dans le ROB) ;
* Dispath dans ou depuis la station de réservation.
* Exécution de l'instruction ;
* Écriture du résultat dans le ROB ;
* Écriture dans le banc de registre physique.
Les instructions sont chargées par blocs de 16 octets, avec un système de fusion de blocs pour gérer les instructions à cheval sur deux blocs. Lors d'un branchement, deux blocs doivent être chargés si l'instruction de destination n'est pas alignée sur 16 octets et cela cause un délai de un cycle d'horloge.
Le décodage des instructions x86 était géré par plusieurs décodeurs. Il y avait trois décodeurs : deux décodeurs simples, et un décodeur complexe. Les décodeurs simples décodaient les instructions les plus fréquentes, mais aussi les plus simples, qui étaient décodées en une seule micro-opération. Les instructions CISC complexes étaient gérées uniquement par le décodeur complexe, basé sur un microcode, qui pouvait fournir jusqu'à 4 micro-opérations par cycle. Le tout est résumé avec la règle 4-1-1. La toute première instruction chargée depuis la file d'instruction va dans le premier décodeur simple. Si jamais le décodeur ne peut pas décoder l'instruction, l'instruction est redirigée dans un autre décodeur, avec un délai d'un cycle d'horloge.
Les stations de réservations étaient regroupées dans une structure centralisée, en sortie de l'unité de renommage. Elles avaient 5 ports d'émission, qui étaient sous-utilisés en pratique. Niveau ALU, on trouve deux ALUs entières, une flottante, une unité pour les instructions SSE et autres, et trois unités pour les accès mémoire (regroupées en une seule unité dans le schéma ci-dessous). Les unités mémoire regroupent une unité de calcul d'adresse pour les lectures, une autre pour les écritures, et une unité pour la gestion des données à écrire. Les unités de calcul d'adresse sont des additionneurs à 4 opérandes, complétement différents des ALU entières. Les ALU entières sont deux unités asymétriques : une ALU simple, et une ALU complexe incorporant un multiplieur. Les deux peuvent exécuter des opérations d'addition, soustraction, comparaison, etc.
[[File:P6 func diag.png|centre|vignette|upright=2|P6 func diag]]
Les premiers Pentium 3 n'avaient pas de cache L2 dans le processeur, celui-ci était sur la carte mère. Mais il a été intégré dans le processeur sur la seconde version du Pentium 3, la version Coppermine.
Le Pentium 3 a servi de base aux microarchitectures d'Intel qui ont suivi. Les changements à chaque nouvelle génération sont assez mineurs : la prédiction de branchement est améliorée, la taille des stations de réservation et du ROB augmente, idem avec les autres structures liées à l'exécution dans le désordre. Les processeurs Intel ont conservé une fenêtre d'instruction centralisée, alors qu'AMD utilise une autre méthode, comme nous allons le voir dans ce qui suit. Les seuls changements notables sont est le passage à un renommage dans le ROB à un renommage à banc de registre physique, ainsi que l'introduction du cache de micro-opération. Et ce sont des modifications qu'AMD a aussi faites, celle-ci étant clairement une bonne idée pour toutes les micro-architectures avec un budget en transistor suffisant. Il est intéressant de garder cela en tête, car une bonne partie des améliorations de chaque micro-architecture proviendra de là.
===La microarchitecture Core===
La '''microarchitecture Core''' fait suite au Pentium 4, mais reprend en fait beaucoup d’éléments du Pentium 2 et 3. Elle utilise la station de réservation unique avec renommage dans le ROB, provenant du Pentium 2/3. Elle supporte aussi les optimisations des opérations ''load-up'', avec notamment un support des macro-opérations mentionnées plus haut. Les améliorations sont assez diverses, mais aussi assez mineures.
* Le processeur incorpore un cache L2, en plus des caches L1 déjà présents auparavant.
* La prédiction de branchement a été améliorée avec notamment l'ajout d'une ''Fetch Input Queue''.
* L'architecture Core passe à la quadruple émission, soit une instruction de plus que sur le Pentium 2 et 3. Pour cela, un quatrième décodeur est ajouté, il s'agit d'un décodeur simple qui ne fournit qu'une seule micro-opération en sortie.
* Un ''stack engine'' et un ''Loop Stream Detector'' ont été ajoutés, ainsi que le support de la macro-fusion qui fusionne une instruction de test et le branchement qui suit en une seule micro-opération.
* Les techniques de désambiguïsation mémoire sont implémentées sur cette micro-architecture.
Il y a quelques modifications au niveau de l'unité de chargement. La file d'instruction a toujours ce système de fusion de blocs, sauf que les branchements ne causent plus de délai d'un cycle lors du chargement. La file d'instruction est suivie par un circuit de prédécodage qui détermine la taille des instructions et leurs frontières, avant de mémoriser le tout dans une file de 40 instructions.
La station de réservation dispose de 6 ports d'émission, mais on devrait plutôt dire 5. Sur les 5, il y en a deux pour les accès mémoire : un pour les lectures, un autre pour les écritures. Les trois ports d'émission restant sont connectés aux unités de calcul. Les unités entières et flottantes sont réparties de manière à ce que chaque port d'émission soit relié à une unité entière et une flottante, au minimum. Ce faisant, le processeur peut émettre trois opérations flottantes, trois opérations entières, un mix d'opérations entières et flottantes. Il y a un additionneur et un multiplieur flottants, sur des ports différents. Tous les ports sont reliés à une ALU simple. Le multiplieur entier est relié au second port d'émission, celui sur lequel se trouve l'additionneur flottant.
Pour les accès mémoire, le processeur contient deux unités de calcul d'adresse : une pour les écritures, une autre pour les lectures. Les deux sont reliées à une ''Load/Store Queue'', appelée ''Memory Ordering Buffer''. Elle est elle-même reliée au cache de données par deux ports : un port de lecture et un port d'écriture.
[[Image:Intel Core2 arch.svg|centre|vignette|upright=2|Intel Core microarchitecture]]
===Les microarchitectures Sandy Bridge and Ivy Bridge===
Les micro-architectures suivant la micro-architecture Core ont introduit quelques grandes modifications : le passage à un renommage à banc de registre physique, l'ajout d'un cache de micro-opérations (et d'un ''Loop Stream Detector'').
L'ajout du cache de micro-opérations est un gros changement, particulièrement avec le jeu d’instruction x86. Le décodage des instructions est lent, couteux en énergie. Mais avec l'introduction du cache de micro-opération, la majorité des micro-opérations est non pas décodée, mais lue depuis le cache de micro-opérations. Les décodeurs décodent les instructions pas encore exécutées, mais les exécutions suivantes sont lues depuis le cache de micro-opérations. Et vu la grande présence de boucles, le cache de micro-opérations est l'alimentation principale du pipeline.
Les décodeurs servent surtout à alimenter le cache de micro-opérations, parfois décoder quelques instructions isolées exécutées de-dehors de boucles, pas plus. Concrètement, ils servent pour 10 à 20% des micro-opérations exécutées. Intel a d'ailleurs reflété ce fait dans sa terminologie. Intel distingue deux voies de chargement : le ''legacy pipeline'' et le cache de micro-opérations. L'unité de chargement et les décodeurs sont regroupés dans la voie du ''legacy pipeline''.
Le cache de micro-opérations est complété avec un ''Loop Stream Detector'', placé après le cache en question. Les décodeurs et le cache de micro-opérations alimentent une file de micro-opérations, située juste avant l'étage de renommage de registres. La file de micro-opérations sert en quelque sorte de tampon entre l'étage de "décodage" et celui de renommage. Le ''Loop Stream Detector'' utilise cette file de micro-opérations comme d'un cache lorsqu'une boucle est détectée. Les micro-opérations de la boucle sont lue depuis la file de micro-opérations, pour être envoyée au renommeur de registres.
L'avantage est que le cache de micro-opérations et/ou les décodeurs sont mis en pause et clock-gatés lorsqu'une boucle s'exécute, ce qui réduit la consommation du processeur. Le ''Loop Stream Detector'' et le cache de micro-opération ont globalement le même effet : désactiver tout ce qui est avant, le ''Loop Stream Detector'' appliquant cette méthode au cache de micro-opération lui-même..
Voyons maintenant quelles sont les micro-architectures qui implémentent ces optimisations.
Les microarchitectures '''Sandy Bridge''' and '''Ivy Bridge''' sont similaires à l'architecture Core, si ce n'est pour le passage à un renommage à banc de registre physique, et l'ajout d'un cache de micro-opérations. Le nombre de ports d'émission passe à 7, avec 4 pour les instructions arithmétiques (flottantes comme entière), 2 pour les lectures, et un pour les écritures (en fait deux, avec un pour le calcul d'adresse, l'autre pour la donnée à écrire). Pour le reste, rien ne change si ce n'est la prédiction de branchement
Les architectures '''Haswell''' et '''Broadwell''' ont ajouté quelques unités de calcul, élargit la sortie du cache de micro-opérations. Un port d'émission pour opération entières a été ajouté, de même qu'un port pour les accès mémoire. Le processeur passe donc à 8 ports d'émission, ce qui permet d'émettre jusqu'à 8 micro-opérations, à condition que le cache de micro-opération suive. Pour le reste, le processeur est similaire aux architectures précédentes, si ce n'est que certaines structures grossissent.
L'architecture '''Skylake''' réorganise les unités de calcul et les ports d'émission pour gagner en efficacité. Pour le reste, les améliorations sont mineures. A la rigueur, l'unité de renommage de registre ajoute des optimisations comme l'élimination des MOV, les idiomes liés aux opérations avec zéro, etc.
===Les microarchitectures récentes d'Intel===
Les architectures '''Ice Lake''' et '''Tiger Lake''' passent de quadruple émission à la pentuple émission. Par contre, le processeur utilise toujours 4 décodeurs. Mais les micro-opérations étant émises depuis le cache de micro-opérations, ce n'est pas un problème pour la pentuple émission. Le processeur peut parfaitement émettre 5 micro-opérations en même temps, si elles sont lues depuis le cache de micro-opérations. Là encore, on voit à quel point le cache de micro-opération découple ce qu'il y avant de ce qu'il y a après.
La microarchitecture Golden Cove, la plus récente, altère les décodeurs et l'unité de chargement. Sur toutes les générations précédentes, on reste sur une unité de chargement qui charge 16 octets à la fois et il y a toujours 4 décodeurs identiques aux générations précédentes. Golden Cove passe à 6 décodeurs simples, et double la taille du chargement qui passe à 32 octets. Une telle stagnation sur les unités de chargement et de décodage s'explique encore une fois par la présence du cache de micro-opération fait que ce n'est pas trop un problème. Tout ce qui précède le cache de micro-opérations n'a pas de raison d'évoluer, car ce cache est très puissant.
==Un étude des microarchitectures superscalaires x86 d'AMD==
Les architectures Intel ont évolué progressivement, sans grandes cassure. Il y a une continuité presque initerrompue entre l'architecture du Pentium 2 et les architectures modernes. Intel a fait des améliorations mineures à chaque nouvelle micro-architecture, si on omet le passage à un renommage à banc de registre physique et l'ajout du cache de micro-opération. A l'opposé, les architectures AMD ont eu de nombreuses cassures dans la continuité où AMD a revu sa copie de fond en comble.
Étudier ces architectures demande de voir trois choses séparément : le ''front-end'' qui regroupe l'unité de chargement et les décodeurs, le ''back-end'' qui gère l'exécution dans le désordre et les unités de calcul, et le sous-système mémoire avec les caches et la ''Load Store Queue''. Leur étude sera plus ou moins séparée dans ce qui suit, pour chaque classe d'architecture.
===La première génération de CPU AMD : les architectures K5, K6, K7, K8 et K10===
La première génération de processeurs AMD est celle des architectures K5, K6, K7, K8 et K10. Il n'y a pas de K9, qui a été abandonné en cours de développement. Les processeurs K5 et K6 portent ce nom au niveau commercial. Par contre, les processeurs d'architecture K7 sont aussi connus sous le nom d''''AMD Athlon''', les AMD K8 sont connus sous le nom d''''AMD Athlon 64''', et les architecture K10 sont appelées les '''AMD Phenom'''. Comme le nom l'indique, l'architecture K8 a introduit le 64 bits chez les processeurs AMD.
Elles ont une architecture assez similaire pour ce qui est du chargement et des caches. Toutes disposent d'au minimum un cache L1 d'instruction et d'un cache L1 de données. Le K5 n'avait que ces caches, mais un cache L2 a été ajouté avec le K7, puis un L3 avec le K10. L'AMD K5 avait une TLB unique, mais les processeurs suivants avaient une TLB pour le L1 d'instruction et une autre pour le L1 de données. Idem pour le cache L2, avec deux TLB : une pour les données, une pour les instructions.
Les caches L1/L2 sont de type exclusifs, à savoir que les données dans le L1 ne sont pas recopiées dans le L2. Le cache L2 est précisément un cache de victime, qui mémorise les données/instructions, évincées des caches L1 lors du remplacement des lignes de cache. L'introduction du cache L2 a entrainé l'ajout de deux TLB de second niveau : une L2 TLB pour les données et une autre pour les instructions. Les architectures K8 et K10 ont ajouté un cache L3, avec un accès indirect à travers l'interface avec le bus.
: L'AMD K7 originel, aussi appelée Athlon classique, n'avait pas de cache L2, mais celui-ci était placé sur la carte mère et fonctionnait à une fréquence moitié moindre de celle du CPU. L'Athlon Thunderbird, puis l'Athlon XP, ont intégré le cache L2 dans le processeur.
{|class="wikitable"
|-
! Architecture AMD
! colspan="5" | Caches
|-
| rowspan="2" | K5
| L1 instruction || L1 données || colspan="3" |
|-
| colspan="2" | TLB unique || colspan="3" |
|-
| colspan="4" |
|-
| rowspan="2" | K6
| L1 instruction || L1 données || colspan="3" | L2 unifié
|-
| TLB L1 instruction || TLB L1 données || colspan="3" |
|-
| colspan="6" |
|-
| rowspan="2" | K7, K8
| L1 instruction || L1 données || colspan="2" | L2 unifié ||
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|-
| colspan="6" |
|-
| rowspan="2" | K10
| L1 instruction || L1 données || colspan="2" | L2 unifié || L3
|-
| TLB L1 instruction || TLB L1 données || TLB L2 instruction || TLB L2 données ||
|}
Fait important, les architectures K5 à K10 utilisent la technique du '''prédécodage''', où les instructions sont partiellement décodées avant d'entrer dans le cache d'instruction. Le prédécodage facilite grandement le travail des décodeurs d'instruction proprement dit. Par contre, le prédécodage prend de la place dans le cache L1 d'instruction, une partie de sa capacité est utilisé pour mémoriser les informations prédécodées. C'est donc un compromis entre taille du cache et taille/rapidité des décodeurs d'instruction.
Sur les architectures K5 et K6, le prédécodage précise, pour chaque octet, si c'est le début ou la fin d'une instruction, si c'est un octet d'opcode, en combien de micro-opérations sera décodée l'instruction, etc. A partir de l'AMD K7, le prédécodage reconnait les branchements inconditionnels. Lorsqu'un branchement inconditionnel est pré-décodé, le pré-décodage tient compte du branchement et continue le pré-décodage des instructions à partir de la destination du branchement. Le système de prédécodage est abandonnée à partir de l'architecture Bulldozer, qui suit l'architecture K10.
La prédiction de branchement de ces CPU tire partie de ce système de pré-décodage, à savoir que les prédictions de branchement sont partiellement mémorisées dans les lignes de cache du L1 d'instruction. Par exemple, l'AMD K5 se passe de ''Branch Target Buffer'' grâce à cela. Si une ligne de cache contient un branchement, elle mémorise l'adresse de destination de ce branchement, en plus des bits de pré-décodage. Si il y a plusieurs branchements dans une ligne de cache, c'est l'adresse de destination du premier branchement pris dans cette ligne de cache qui est mémoirsée.
Un défaut de cette approche est que si le branchement n'est pas dans le L1 d'instruction, aucune prédiction de branchement ne peut être faite et le préchargement ne peut pas fonctionner. C'est une limitation que n'ont pas les BTB découplées du cache L1 : elles peuvent prédire un branchement qui a été évincé dans le L2 ou le L3, tant que l'entrée associée est dans le BTB. Les prédictions peuvent même servir à précharger les instructions utiles.
[[File:Comparaison du chargement de l'AMD K5 et K6.png|centre|vignette|upright=2|Comparaison du chargement de l'AMD K5 et K6]]
Au niveau du décodage, on trouve de nombreuses différences entre les premières architectures AMD. L'AMD K5 contient 4 décodeurs hybrides, afin de décoder 4 instructions par cycles. Le K5 a quatre décodeurs simples couplés à 4 décodeurs complexes avec chacun un accès au micro-code. Une instruction peut donc passer par a donc deux voies de décodage : un décodage rapide et simple pour les instructions simples, un décodage lent et passant par le microcode pour les instructions complexes. Pour décoder 4 instructions, les deux voies sont dupliquées en 4 exemplaires, ce qui a un cout en circuits non-négligeable.
L'AMD K6 utilise moins de décodeurs et ne peut que décoder deux instructions à la fois maximum. Par contre, il fournit en sortie 4 micro-opérations. Il intègre pour cela deux décodeurs simples, un décodeur complexe et un décodeur micro-codé. Un décodeur simple transforme une instruction simple en une ou deux micro-opérations. Il est possible d'utiliser les deux décodeurs simples en même temps, afin de fournir 4 micro-opérations en sortie du décodeur. Les deux autres décodent une instruction complexe en 1 à 4 micro-opérations. Si jamais la ou les deux instructions sont décodées en 1, 2 ou 3 micro-opérations, les micro-opérations manquantes pour atteindre 4 sont remplies par des NOPs.
Pour le K7 et au-delà, le processeur dispose de décodeurs séparées pour les instructions micro-codées de celles qui ne le sont pas. Le processeur peut décoder jusqu’à 3 instructions par cycle. Le décodage d'une instruction microcodée ne peut pas se faire en parallèle du décodage non-microcodé. C'est soit le décodeur microcodé qui est utilisé, soit les décodeurs câblés, pas les deux en même temps. Le décodage d'une instruction prend 4 cycles. Les instructions non-microcodées sont décodées en une seule micro-opération, à un détail près : le CPU optimise la prise en charge des instructions ''load-up''.
La différence entre le K6 et le K7 s'explique par des optimisations des instructions ''load-up''. Sur le K6, les instructions ''load-up'' sont décodées en deux micro-opération : la lecture en RAM, l'opération proprement dite. Mais sur le K7, une instruction ''load-up'' est décodée en une seule micro-opération. En conséquence, les décodeurs simples sont fortement simplifiés et le décodeur complexe disparait au profit d'un microcode unique.
[[File:Décodage sur le K5 et le K5.png|centre|vignette|upright=3|Décodage sur le K5 et le K5]]
====Les microarchitectures K5 et K6 d'AMD====
Les deux premières architectures étaient les architectures K5 et K6, l'architecture K6 ayant été déclinée en quatre versions, nommées K6-1, K6-2, et K-3, avec une version K6-3 bis. Elles sont regroupées ensemble car elles ont beaucoup de points communs. Par exemple, tout ce qui a trait au chargement et au cache était similaire, de même que les unités de calcul.
Les deux architectures avaient n'avaient pas de cache L2 et devaient se contenter d'un cache L1 d'instruction et d'un cache L1 de données. L'AMD K5 incorpore une TLB unique, alors que le K6 utilise des TLB séparées pour le cache d'instruction et le cache de données. Une différence entre l'architecture K5 et K6 est que la première utilise des caches normaux, alors que la seconde utilise des ''sector caches''.
Les deux architectures disposaient des unités de calcul suivantes : deux ALU entières, une FPU, deux unités LOAD/STORE pour les accès mémoire, une unité de branchement et une ou plusieurs unités SIMD. Une organisation classique, donc.
Pour les unités entières, il y avait deux ALU simples, un ''barrel shifter'' et un diviseur. Il n'y a pas d'erreur, le processeur incorpore un circuit diviseur, mais pas de circuit multiplieur. La raison est que la multiplication est réalisée par la FPU ! En effet, le multiplieur flottant de la FPU intègre un multiplieur entier pour multiplier les mantisses, qui est utilisé pour les multiplications entières. La même technique a été utilisée sur l'Atom, comme vu plus haut. Le tout était alimenté par deux ports d'émission, appelés ports X et Y. Sur l'architecture K5, le ''barrel shifter'' et le diviseur sont des ports différents.
{|class="wikitable"
|+ AMD K5
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
| ''Barrel Shifter''
| Diviseur
|}
Sur l'architecture K6, le ''barrel shifter'' et le diviseur sont sur le même port.
{|class="wikitable"
|+ AMD K6
|-
! Port X
! Port Y
|-
| ALU simple
| ALU simple
|-
|
| ''Barrel Shifter''
|-
|
| Diviseur
|}
Niveau unités mémoire, le K5 avait deux unités LOAD/STORE, chacune capable de faire lecture et écriture. Par contre, la ''store queue'' n'a qu'un seul port d'entrée, ce qui fait que le processeur peut seulement accepter une écriture par cycle. Le processeur peut donc émettre soit deux lectures simultanées, soit une lecture accompagnée d'une écriture. Impossible d'émettre deux écritures simultanées, ce qui est de toute façon très rare. L'architecture K6 utilise quant à elle une unité LOAD pour les lectures et une unité STORE pour les écritures. Ce qui permet de faire une lecture et une écriture par cycle, pas autre chose.
Niveau unités SIMD, l'architecture K7 n'avait qu'une seule unité SIMD, placée sur le port d'émission X. L'architecture K8 ajouta une seconde unité SIMD, sur l'autre port d'émission entier. De plus, trois ALU SIMD ont été ajoutées : un décaleur MMX, une unité 3DNow!, une unité mixte MMX/3DNow. Elles sont reliées aux deux ports d'émission entier X et Y ! Elles ne sont pas représentées ci-dessous, par souci de simplicité.
[[File:Unité de calcul des processeurs AMD K5 et K6.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K5 et K6. les unités sur la même colonnes sont reliées au même port d'émission.]]
Si les unités de calcul et le chargement sont globalement les mêmes, les deux architectures se différencient sur l'exécution dans le désordre. L'AMD K5 utilise du renommage de registre dans le ROB avec des stations de réservation. Par contre, l'AMD K6 utilise une fenêtre d'instruction centralisée. De plus, son renommage de registre se fait avec un banc de registre physique.
L'architecture AMD K5 utilisait de deux stations de réservation par unité de calcul, sauf pour les deux unités mémoire partageaient une station de réservation unique (deux fois plus grande). Les stations de réservation sont cependant mal nommées, vu que ce sont en réalité des mémoire FIFO. Une micro-opération n'est émise que si elle est la plus ancienne dans la FIFO/station de réservation. Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique. Le tampon de ré-ordonnancement faisait seulement 16 instructions.
[[File:AMD K5.jpg|centre|vignette|upright=3|AMDK5 Diagramme.]]
L'architecture K6 remplace les stations de réservations par une fenêtre d'instruction centralisée. Les 4 micro-opérations renommées sont écrites dans la fenêtre d'instruction par groupe de 4, NOP de ''padding'' inclus. La fenêtre d'instruction centralisé contient 24 micro-opérations, groupées en 6 groupes de 4 micro-opérations, avec potentiellement des NOP dedans suivant le résultat du décodage. L'avantage est que l'implémentation de la fenêtre d'instruction est simple. La fenêtre d'instruction centralisée permettait d'émettre 6 micro-opérations en même temps (une par unité de calcul/mémoire). Le renommage de registres se faisait dans le tampon de ré-ordonnancement, il n'y avait pas encore de banc de registre physique.
Le processeur utilisait un renommage avec un banc de registre physique. Le banc de registre physique pour les entiers contenait 48 registres, dont 24 étaient des registres architecturaux et 24 étaient des registres renommés. Sur les 24 registres architecturaux, 16 avaient une fonction de ''scratchpad'' que les ''datasheets'' d'AMD ne détaillent pas, les 8 restants étaient les registres généraux EAX, EBX, etc.
[[File:AMD K6 Little foot & Modl 6.png|centre|vignette|upright=3|AMD K6 original.]]
====Les microarchitectures K7, K8 et K10 d'AMD====
Les microarchitectures suivantes sont les architectures K7, K8 et K10. Les architectures K7, K8 et K10 sont assez similaires. La différence principale entre le K7 et le K8 est le support du 64 bits. Les apports du K10 sont la présence d'un cache L3, d'une unité de calcul supplémentaire et d'améliorations de la prédiction de branchement. La taille de certains caches a été augmentée, de même que la largeur de certaines interconnexions/bus.
L'architecture K7 des processeurs Athlon utilisait le renommage de registre, mais seulement pour les registres flottants, pas pour les registres entiers. Le ranommeg des registres flottants étaient réalisé via un banc de registres physique, ne contenant que des registres flottants. Les architectures K8 et K10 utilisent le renommage de registres pour tous les registres, entiers comme flottants. Par contre, le renommage de registre n'est pas réalisé de la même manière pour les registres entiers et flottants. Les registres entiers sont renommés dans le tampon de ré-ordonnancement, comme c'était le cas sur les architectures Intel avant le Pentium 4. Par contre, les registres flottants sont renommés grâce à un banc de registre physique. Le K8 est donc un processeur au renommage hybride, qui utilise les deux solutions de renommage principales.
A partir du K7, le CPU optimise la prise en charge des instructions ''load-up''. Les instructions ''load-op'' sont appelées des macro-opérations dans la terminologie d'AMD, et aussi d'Intel. L'idée est que les instructions ''load-up'' sont décodées en micro-opérations intermédiaires. Elles sont propagées dans le pipeline comme étant une seule micro-opération, jusqu'à l'étage d'émission. Lors de l'émission, les instructions ''load-up'' sont scindées en deux micro-opérations : la lecture de l'opérande, puis l'opération proprement dite. Faire ainsi économise des ressources et optimise le remplissage du tampon de ré-ordonnancement, des fenêtres d'instructions, des stations de réservation, etc.
Le tampon de réordonnancement est combiné avec divers circuits en charge de l'exécution dans le désordre, dans ce qui s'appelle l'''instruction control unit''. Il contient de 72 à, 84 instructions, qui sont regroupées en groupes de 3. Là encore, comme pour le K5 et le K6, le tampon de réordonnancement tient compte de la sortie des décodeurs. Les décodeurs fournissent toujours trois micro-opérations par cycle, quitte à remplir les vides par des NOP. Le tampon de réordonnancement reçoit les micro-opérations, NOP inclus, par groupes de 3, et est structuré autour de ces triplets de micro-opération, y compris en interne.
Pour ce qui est de l'unité mémoire, elle est précédée par une file de µops mémoire, qui émet les accès mémoire dans l'ordre du programme. Elle est souvent qualifiée de ''Load-Store Queue'', mais ce n'est pas la terminologie que nous utilisons dans ce cours. La file de micro-opération lire/écrire 64 bits par cycle depuis le cache L1, ce qui fait un seul accès au cache par cycle. La file de µops mémoire est appelée la ''Pre-Cache Queue''. Si au vu de son nom, vous avez deviné qu'il y avait une ''Post-Cache Queue''. Elle mémorise les lectures/écritures émises, mais qui ont levé un défaut de cache L1. Elle ne fait pas partie de la file de µops mémoire proprement dite.
Les architectures K7, K8 et K10 ont des unités de calcul très similaires. Concrètement, il y a trois ALU entières, trois unités de calcul d'adresse, et une FPU. Le processeur incorpore, aussi un multiplieur entier, relié sur le port d'émission de la première ALU. La FPU regroupe un additionneur flottant, un multiplieur flottant, et une troisième unité LOAD/STORE pour les lectures/écritures pour les nombres flottants. L'architecture K8 ajoute une unité de manipulation de bit, la K10 un diviseur entier.
[[File:Unité de calcul des processeurs AMD K7, K8 et K10.png|centre|vignette|upright=2|Unité de calcul des processeurs AMD K7, K8 et K10]]
La manière d'alimenter les ALU en micro-opérations varie un petit peu entre les architectures K7, K8 et K10. Il y a cependant quelques constantes entre les trois. La première est qu'il y a une fenêtre d'instruction séparée pour les flottants, de 36 à 42 entrées, avec renommage de registre. La fenêtre d'instruction flottante a trois ports d'émission : un pour l'additionneur flottant, un autre pour le multiplieur, et un troisième pour la troisième unité flottante qui s'occupe du reste. La seconde est que chaque ALU entière est couplée avec une unité de calcul d'adresse. Par contre, la méthode de couplage varie d'un processeur à l'autre.
: Les stations de réservation sont nommées des ''schedulers'' dans les schémas qui suivent.
La microarchitecture K7 avait deux fenêtres d'instruction : une pour les opérations flottantes, une autre pour les instructions entières et les accès mémoire. La fenêtre d'instruction entière était reliée à 3 ALU entières et à 3 AGU. Elle pouvait émettre trois micro-opérations en même temps : trois micro-opérations entières, trois micro-opérations mémoire. Les AGU étaient reliées à la file de µops mémoire mentionnée plus haut, ce qui permet d'émettre trois µops mémoire par cycle. Par contre, la file de µops mémoire ne pouvait exécuter qu'une lecture de 64 bits ou une écriture de 64 bits. En clair, trois micro-opérations mémoire peuvent être émises par cycle, cela entraine trois calculs d'adresse simultanés, mais les trois lectures/écritures sont mises en attente dans la file de µops mémoire. Elles s'exécutent alors l'une après l'autre.
La fenêtre d'instruction entière contenait 5 à 6 groupes de 3 macro-opérations. Vous noterez que j'ai parlé de macro-opérations et pas de micro-opérations, car les instructions ''load-up'' sont considérées comme une seule "micro-opération" dans la fenêtre d'instruction entière. Et cela se marie bien avec une fenêtre d'instruction unique partagée entre pipeline entier et pipeline mémoire. Une macro-opération était scindée en deux micro-opérations : une micro-opération mémoire et une micro-opération entière. Il est donc avantageux de regrouper unités mémoire et unités entières à la même fenêtre d'instruction pour ce faire.
[[File:AMD K7.png|centre|vignette|upright=3|AMD K7]]
Sur les architectures K8 et K10, la station de réservation unique de 15 micro-opérations est remplacée par trois stations de réservations, de 8 micro-opérations chacune pour le K8, de 10 pour le K10. Chaque station de réservation entière alimente une unité de calcul entière et une unité de calcul d'adresse. l'unité de calcul d'adresse est reliée à la file de µops mémoire, qui n’exécute toujours qu'un seul accès mémoire par cycle. Le multiplieur est relié à la première station de réservation, sur le même port d'émission que l'ALU.
[[File:AMD Husky microarchitecture.png|centre|vignette|upright=3|AMD Husky microarchitecture]]
La microarchitecture K10 a été déclinée en plusieurs versions, nommées Grayhound, Grayhound+ et Husky, Husky étant une architecture gravée en 32 nm dédiée aux processeurs A-3000. L'architecture Grayhound a plus de cache et un ROB plus grand, la Husky est quand à elle un peu plus différente. Elle n'a pas de cache L3, contrairement aux autres architectures K10, ce qui simplifie fortement son sous-système mémoire. Par contre, les fenêtres d'instructions/stations de réservation et le ROB sont plus grands, pareil pour les files dans l'unité mémoire. Une ALU pour les divisions entières a aussi été ajoutée.
Pour résumer, les architectures K7, K8 et K10 séparent les pipelines entiers et flottants : trois pipelines entiers avec chacun son unité de calcul, et un pipeline flottant avec plusieurs unités de calcul. Les raisons à cela sont assez diverses. Disons que dupliquer des ALU entières simples prend peu de transistors, là où les gros circuits comme le multiplieur ou la FPU ne sont pas dupliqués.
Et cela a un autre avantage : le renommage, ''dispatch'' et l'émission sont plus simples. Les pipelines entiers ont une exécution dans le désordre peu complexe, grâce au grand nombre d'unités de calcul, ce qui fait que le pipeline entier est de seulement 15 cycles au total (chargement et décodage inclus). A l'opposé, la FPU est alimentée par une exécution dans le désordre très complexe, avec banc de registre physique et beaucoup de ressources, mais au prix d'un pipeline flottant plus long de 3 cycles, soit 18 cycles au total.
===Les microarchitectures ZEN d'AMD===
Viennent ensuite les '''microarchitectures Bulldozer''', avec trois révisions ultérieures nommées Piledriver, Steamroller et Excavator. Mais du fait de l'utilisation de techniques de multithreading matériel que nous n'avons pas encore abordé, nous ne pouvons pas en parler ici.
Les microarchitectures suivantes sont les '''architectures ZEN 1/2/3/4/5'''. Elles se ressemblent beaucoup, chacune accumulant les améliorations des précédentes. Mais le cœur de l'architecture reste plus ou moins le même. En passant à la suivante, le nombre de registre virtuel augmente, le ''branch target buffer'' augmente en taille, le ROB et les files d'attente grossissent, les caches de micro-opération aussi, les caches grossissent, etc. Une optimisation intéressante est l'ajout d'un cache de micro-opération, qui améliore grandement les performances du ''front-end'', notamment pour les boucles.
La microarchitecture Zen 1 est illustrée ci-dessous. Comme on le voit, les registres flottants ont une unité de renommage séparée de celle pour les entiers, mais les deux utilisent du renommage à banc de registre physique. Il y a par contre une différence au niveau des fenêtres d'instruction, notées ''scheduler'' dans le schéma.
Pour ce qui est des unités de calcul flottantes, il y a une fenêtre unifiée qui alimente quatre ALU, grâce à 4 ports d'émission. Mais pour les ALU entières, il y a une fenêtre d'instruction par ALU, avec un seul port d'émission connecté à une seule ALU. La raison de ce choix est que les opérations flottantes ont un nombre de cycle plus élevé, sans compter que les codes flottants mélangent bien additions et multiplication.
Une fois décodées, les instructions sont placées dans une première file de micro-opérations om elles attendent, puis sont dispatchées soit dans le pipeline entier, soit dans le pipeline flottant. les micro-opérations entières sont insérées dans une fenêtre d'instruction directement, alors que les micro-opérations flottantes doivent patienter dans une seconde file de micro-opérations.
La raison est que les micro-opérations flottantes ayant une grande latence, trop d'instructions flottantes consécutives pourraient bloquer le pipeline flottant, sa fenêtre d'instruction étant pleine. Le pipeline flottant étant bloqué, la première file de micro-opérations serait bloquée et on ne pourrait plus émettre de micro-opérations entières. Pour éviter cela, une solution serait d'agrandir la file de micro-opérations, mais cela la rendrait plus lente et se ferait donc au détriment de la fréquence d'horloge. Alors une solution a été d'ajouter une seconde file de micro-opérations, au lieu d'agrandir la première.
[[File:Zen microarchitecture.svg|centre|vignette|upright=3|Microarchitecture Zen 1 d'AMD.]]
Le passage à la microarchitecture n'a pas causé de grands changements. Le Zen 2 a ajouté une unité de calcul d'adresse, ce qui fait qu'on passe à 4 ALU, 3 AGU et 4 FPU. La fenêtre d'instruction flottante reste la même. Par contre, les fenêtres d'instruction entières changent un peu. Ou plutot devrais-je dire les fenêtres d'instruction mémoire. En effet, le Zen 2 fusionne les fenêtres d'instructions liées aux AGU en une seule fenêtre d'instruction deux fois plus grosse.
Le Zen 5 a ajouté deux autres ALU entières et une unité de calcul d'adresse (6 ALU / 4 AGU)
==Les processeurs Atom d'Intel, de microarchitecture Bonnell==
L'architecture de l'Atom première génération est assez simple. Son pipeline faisait 16 étages, ce qui est beaucoup. C'est un processeur 32 bits, ce qui aura son importance dans ce qui suit. Il était conçu pour être un processeur basse consommation, donc peu puissant. En conséquence, il n'a pas d'exécution dans le désordre, même s'il est superscalaire. C'était la norme à l'époque pour les processeurs basse consommation, que de faire sans exécution dans le désordre. De nos jours, les choses ont bien changée, même les processeurs basse consommation ont exécution dans le désordre, superscalarité et renommage de registres.
===Le ''front-end'' de l'Atom===
Le cache d'instruction permet de lire 8 octets par cycle, qui sont placés dans une file d'instruction, elle-même suivie par deux décodeurs. Le fait que les décodeurs lisent les instructions depuis une file d'instruction fait que les deux instructions décodées ne sont pas forcément consécutives en mémoire RAM. Par exemple, l'Atom peut décoder un branchement prédit comme pris, suivi par l'instruction de destination du branchement. Les deux instructions ont été chargées dans la file d'instruction et sont consécutifs dedans, alors qu'elles ne sont pas consécutives en mémoire RAM.
Sur l'Atom, la majorité des instructions x86 sont décodées en une seule micro-opération, y compris les instructions ''load-up''. Le microcode n'est utilisé que pour une extrême minorité d'instructions et est à part des deux décodeurs précédents. L'avantage est que cela permet d'utiliser au mieux la file de micro-opération, qui est de petite taille. Mais surtout, cela permet de grandement réduire la consommation du processeur, au détriment de ses performances.
Pour avoir un décodage rapide, malgré des instructions complexes, le processeur recourt à la technique du pré-décodage, qui prédécode les instructions lors de leur chargement dans le cache d'instruction. Le prédécodage lui-même prend deux cycles, là où une lecture dans le L1 d'instruction en prend 3. les défauts de cache d'instruction sont donc plus longs de deux cycles. Mais l'avantage du prédécodage est que la consommation d'énergie est diminuée. Prenez une instruction exécutée plusieurs fois, dans une boucle. Au lieu de décoder intégralement une instruction à chaque fois qu'on l'exécute, on la prédécode une fois, seul le reste du décodage est fait à chaque exécution. D'où un gain d'énergie assez intéressant. Les caches de micro-opération, qui sont capables d'exécuter une optimisation similaire, n'existaient pas encore à cette époque.
===Le chemin de données de l'Atom===
Les deux décodeurs alimentent une file de micro-opérations de petite taille : 32 µops maximum, 16 par ''thread'' si le ''multithreading'' matériel est activé. La file de micro-opérations a deux ports d'émission, ce qui permet d'émettre au maximum 2 µops par cycle. Les conditions pour cela sont cependant drastiques. Les deux instructions ne doivent pas avoir de dépendances de registres, à quelques exceptions près liées au registre d'état. Le multithreading matériel doit aussi être désactivé. Les deux instructions doivent aller chacun dans un port différent, et cela tient en compte du fait que les deux ports sont reliés à des unités de calcul fort différentes. Le tout est illustré ci-dessous.
Les deux ports ont chacun une ALU simple dédiée, capable de faire des additions/soustractions, des opérations bit à bit et des copies entre registres. Mais ils ont aussi des opérations qui leur sont spécifiques. La séparation entre les deux pipelines est assez complexe. Il ne s'agit pas du cas simple avec un pipeline entier et un pipeline flottant séparés. En réalité, il y a deux pipelines, chacun capables de faire des opérations entières et flottantes, mais pas les mêmes opérations.
Le premier port permet d’exécuter des opérations entières simples, une addition flottante, des comparaisons/branchements, ou une instruction de calcul d'adresse LEA. Le second port/pipeline est, quant à lui, conçu pour exécuter les instruction ''load-up'' nativement, en une seule micro-opération. Il contient toute la machinerie pour faire les accès mémoire, notamment des unités de calcul d'adresse et un cache L1 de données. A la suite du cache, se trouvent une ALU entière simple, un ''barrel shifter'', et un circuit multiplieur/diviseur. Le circuit multiplieur/diviseur est utilisé à la fois pour les opérations flottantes et entières.
[[File:Intel Atom Microarchitecture.png|centre|vignette|upright=2.5|Intel Atom Microarchitecture]]
Cette organisation difficile à comprendre est en réalité très efficace, très économe en circuit, tout en gardant une performance intéressante. Les instructions simples, ADD/SUB/bitwise sont supportées dans les deux pipelines. Il faut dire que ce sont des opérations courantes qu'il vaut mieux optimiser au mieux. Le processeur peut donc émettre deux opérations simples et fréquentes en même temps, ce qui augmente les performances.
Les opérations plus complexes, à savoir les multiplications/divisions/décalages/rotations/manipulations de bit sont supportées dans un seul pipeline. La raison est qu'il est rare que de telles opérations soient consécutives, et qu'il n'est donc pas utile d'optimiser pour cette situation. Si les deux pipelines devaient supporter ces opérations, cela demanderait de dupliquer les circuits multiplieurs/diviseur, ce qui aurait un cout en circuit important pour un gain en performance assez faible.
===Le système d'exceptions flottantes de l'Atom===
Le processeur étant sans exécution dans le désordre, ses instructions doivent écrire dans les registres dans l'ordre du programme. En conséquence, certaines instructions doivent être retardées, leur émission doit attendre que les conditions soient adéquates. Et cela pose problème avec les opérations flottantes, vu qu'elles prennent pas mal de cycles pour s'exécuter. Imaginez qu'une instruction flottante de 10 cycles soit suivie par une instruction entière. En théorie, on doit retarder l'émission de l'instruction entière de 9 cycles pour éviter tout problèmes. Le cout en performance est donc assez important.
En théorie, les instructions entières et flottantes écrivant dans des registres séparés, ce qui fait que l'on pourrait exécuter instructions entières et flottantes dans le désordre. Sauf pour les instructions de copie entre registres entier et flottants, mais laissons-les de côté. Le problème est qu'une instruction flottante peut parfois lever une exception, par exemple en cas de division par zéro, ou pour certains calculs précis. Si une exception est levée, alors l'instruction flottante est annulée, de même que toutes les instructions qui suivent, y compris les opérations entières.
Ce n'est pas un problème si le processeur gère nativement les exceptions précises, par exemple avec un tampon de ré-ordonnancement. Mais l'Atom étant un processeur sans exécution dans le désordre, les instructions entières devraient être mises en attente tant qu'une instruction flottante est en cours d'exécution. Heureusement, l'Atom d'Intel a trouvé une parade. La technique, appelée ''Safe Instruction Recognition'' par Intel, est décrite dans le brevet US00525721.6A.
L'idée est de tester les opérandes flottantes, pour détecter les combinaisons d'opérandes à problème, dont l'addition/multiplication peut lever une exception. Si des opérandes à problème sont détectées, on stoppe l'émission de nouvelles instructions en parallèle de l'instruction flottante et l'unité d'émission émet des bulles de pipeline tant que l'instruction flottante est en cours. Sinon, l'émission multiple fonctionne. La technique permet ainsi de ne pas écrire dans les registres entiers/flottants dans l'ordre du programme : une instruction entière peut être autorisée à s'exécuter même si elle écrit dans un registre entier avant qu'une instruction flottante délivre son résultat.
==La microarchitecture Netburst du Pentium 4==
Dans cette section, nous allons voir l'architecture du processeur Pentium 4, qu'on a volontairement laissée de côté précédemment. Pourquoi un tel saut dans le temps ? Parce que le Pentium est complément à part des autres architectures Intel. Le Pentium 4 a représenté une rupture en termes de microarchitecture, qui a été un échec tellement retentissant que les processeurs suivants sont repartis sur la base du Pentium 3.
Il introduisait de nombreuses nouveautés architecturales qui étaient très innovantes. Par exemple, il introduisait le renommage avec un banc de registre physique, qui a été utilisé sur tous les processeurs Intel suivants. Mais la plupart de ces innovations étaient en réalité de fausses bonnes idées, ou du moins des idées difficiles à exploiter. Par exemple, le système de pipeline à ''replay'' n'a été utilisé que sur le Pentium 4 et aucun autre processeur ne l'a implémenté.
===Un focus sur la fréquence d'horloge===
La microarchitecture du Pentium 4 a été déclinée en plusieurs versions, dont les finesses de gravure n'étaient pas les mêmes. La microarchitecture Netburst, utilisée sur le Pentium 4, utilisait un pipeline à 20 étage, augmenté à 32 sur une révision ultérieure. Il a existé quatre révisions de l'architecture : Willamette (180 nm), Northwood (130 nm), Prescott (90 nm) et Cedar Mill (65 nm).
Un point important est que le Pentium 4 était prévu pour fonctionner à haute fréquence. Ses 1,5 GHz étaient impressionnants pour l'époque, les autres processeurs tournant à une fréquence proche du GigaHertzs. Pour cela, la solution retenue par Intel a été un pipeline très long, avec beaucoup d'étages. Le Pentium 4 a été décliné en plusieurs versions assez proches, chacune avec sa propre finesse de gravure qui n'ont pas toute le même pipeline. Les micro-architectures ''Willamette'' et ''Northwood'' avaient un pipeline de 20 étages, alors que les autres processeurs de l'époque avaient entre 10 et 15 étages maximum. Les micro-architectures ''Prescott'' et ''Cedar Mill'' étaient une refonte qui a fait grimper le nombre d'étages à 31 ! Du jamais vu, il s'agit d'un record pour un processeur commercial.
Un pipeline aussi long permet d'exécuter beaucoup d’instructions en même temps, chacune dans un étage, mais aussi d'atteindre de hautes fréquences facilement. Le problème est qu'un pipeline avec autant d'étages a beaucoup de problèmes. Un point important est que la prédiction de branchement est cruciale. Pour rappel, la pénalité en cas de mauvaise prédiction dépend du nombre d'étages avant que le branchement soit résolu. Et les branchements sont résolus soit en fin de décodage, soit dans l'unité de calcul. C'est à dire au milieu du pipeline, soit en fin de pipeline. La pénalité en cas de mauvaise prédiction de branchement était énorme sur le Pentium 4, elle atteignait facilement 30 cycles
Pour compenser, le Pentium 4 avait une prédiction de branchement très performante, pour l'époque. J'insiste sur le pour l'époque. Il utilisait un prédicteur qu'on a déjà abordé dans le chapitre sur la prédiction de branchement, précisément un prédicteur adaptatif à deux niveaux avec un historique global de 16 bits. Il avait aussi un ''Branch Target Buffer'' de 4096 entrées. Mais surtout, il intégrait une sorte de précurseur du cache de micro-opération, appelé le cache de traces, qui est détaillé dans la section suivante.
===Le cache de trace du Pentium 4===
Les décodeurs du Pentium 4 ne font pas décoder les instructions, ils mémorisent le résultat dans un cache de micro-opération un peu particulier, appelé le '''cache de trace'''. Une ligne de cache peut mémoriser 6 micro-opérations, ce qui peu sembler peu mais a été repris sur les micro-architectures suivantes. Mais le cache de trace a une grande différence avec un cache de micro-opération normal. Un cache de micro-opération normal mémorise une instruction par ligne de cache. Une instruction est décodée en plusieurs micro-instructions, qui sont enregistrées dans une ligne de cache. Si l'instruction n'utilise par les 6 micro-opérations disponibles, le reste de la ligne de cache n'est pas utilisé. Mais le Pentium 4 optimise le tout de manière ce à ce que ne soit pas le cas.
Sur le Pentium 4, la contrainte du "une instruction par ligne de cache" est abandonnée. Une ligne de cache mémorise 6 micro-opérations consécutives, qui peuvent appartenir à plusieurs instructions. Par exemple, si le décodeur décode 4 instructions consécutives en 6 micro-opérations au total, alors le tout prendra une seule ligne de cache sur le Pentium 4. Et les 4 instructions consécutives n'ont même pas à être consécutives en mémoire : il peut y avoir des branchements pris entre ces instructions !
{|class="wikitable"
|+ Cache de trace
|-
! Ligne de cache
| ADD || SUB || ADD || MOV || MUL || ''shift''
|-
! Ligne de cache
| colspan="3" | ADD ''load-up'' || MUL || colspan="2" | Branch if Equal
|-
! Ligne de cache
| XOR || colspan="4" | POP || SUB
|-
! ...
| colspan="6" | ...
|}
Pour expliquer cela plus concrètement, nous allons devoir introduire les concepts de trace et de bloc de base. Un '''bloc de base''' (''basic block'') est une suite d'instructions sans branchement, qui est séparé par deux branchements. Le début d'un bloc de base est la destination d'un branchement, un bloc de base se termine avec un branchement. Une '''trace''' est formée en concaténant plusieurs blocs de base. Pour donner un exemple, regardez le code illustré ci-contre. Il est composé d'un bloc de base A, suivi par un bloc de base B, qui peut faire appel soit au bloc C, soit un bloc D. Un tel code peut donner deux traces : ABC ou ABD. La trace exécutée dépend du résultat du branchement qui choisit entre C et D.
Le cache de trace mémorise des traces de 6 micro-opérations consécutives. Les traces sont formées en sortie des décodeurs d'instruction, par de subtiles opérations mélangeant mémorisation, décalage et concaténation. Les circuits qui construisent les traces ne sont pas connus, mais ils doivent certainement être très compliqués. toujours est-il qu'un cache de trace peut mémoriser des traces différentes, même si leur début est le même. Par exemple, prenons deux traces, composées des blocs de base A, B, C et D. La première trace est la trace ABC, la seconde est la trace ABD. Les deux traces auront chacune une ligne de cache dédiée.
Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Pour vérifier cela, le tag du cache de traces contient l'adresse du premier bloc de base, la position des branchements dans la trace et le résultat des prédictions utilisées pour construire la trace. Le résultat des prédictions de branchement de la trace est stocké sous la forme d'une suite de bits : si la trace contient n branchements, le n-ième bit vaut 1 si ce branchement a été pris, et 0 sinon. Même chose pour la position des branchements dans la trace : le bit numéro n indique si la n-ième instruction de la trace est un branchement : si c'est le cas, il vaut 1, et 0 sinon.
Si la trace est réutilisée par la suite, elle est lue depuis le cache de traces. Pour savoir si une trace est réutilisable, l'unité de chargement envoie le ''program counter'' au cache de traces, l'unité de prédiction de branchement fournit le reste des informations. Si on a un succès de cache de traces, et la trace est envoyée directement au décodeur. Sinon, la trace est chargée depuis le cache d'instructions et assemblée. Il faut signaler que le cache de trace avait sa propre unité de prédiction de branchement séparée de l'unité de prédiction de branchement normale.
[[File:TraceCache.png|centre|vignette|upright=2|Cache de traces.]]
Le cache de traces réduisait la longueur du pipeline en cas de succès de cache de trace. Quand les instructions étaient lues depuis le cache de trace, les étages avant le cache de trace ne sont pas utilisés, tout se passe comme s'ils étaient retirés du pipeline. C'est la même chose avec le cache de micro-opération des processeurs modernes, mais l'idée n'existait pas encore à l'époque. Le cache de trace mémorise des traces décodées, ce qui fait qu'un succès de cache de trace contournait non seulement le cache d'instruction, mais aussi les décodeurs. Le temps d'accès au cache de trace pouvait être assez élevé, même s'il était comparable au temps d'accès du cache d'instruction.
Le cache de traces a depuis été remplacé par une alternative bien plus intéressante, le cache de micro-opérations, plus flexible et plus performant. Comparé à un cache de trace, la contrainte "une instruction par ligne de cache" simplifie grandement l'implémentation d'un cache de micro-opération. Ne parlons pas de la détection des succès de cache, qui demande d'utiliser les prédictions de branchement. Mais le vrai problème avec le cache de trace est tout autre. Il arrive souvent qu'une micro-opération soit présente dans plusieurs lignes de cache en raison du processus de construction des traces, chose impossible avec un cache de micro-opération. Et c'est un problème, qui réduit la capacité effective du cache de trace. Alors certes, une ligne de cache est plus remplie que sur un cache de micro-opération, on est certain que les 6 micro-opération par ligne de cache sont remplies. Mais la redondance réduit grandement cet avantage.
===L'exécution dans le désordre et le chemin de données du P4===
Le renommage de registres se fait avec un banc de registres physiques avec une table d'alias.
Le Pentium 4 avait une exécution dans le désordre très limitée, basée sur la présence de deux files de micro-opération : une pour les accès mémoire, une autre pour les autres instructions. La seconde file regroupait opérations entières et flottantes, elles n'étaient pas séparées. Avec ces deux files, les instructions mémoire étaient exécutées dans l'ordre du programme, les instructions arithmétiques s'exécutaient aussi dans l'ordre du programme, mais une instruction arithmétique pouvait passer avant une instruction mémoire et inversement. L'avantage est que cela permettait de faire des lectures en avance, c'était une forme limitée de lecture non-bloquantes.
Il s'agit bel et bien de deux files d'instructions, pas de fenêtres d'instruction ni de stations de réservation. Le Pentium 4 est le seul processeur commercial qui a utilisé des files de micro-opération séparées, tous les autres utilisent des fenêtres d'instruction : centralisées pour Intel, décentralisées pour AMD (en général). Le Pentium 2 et 3, bien qu'antérieurs, utilisait une station de réservation unique. Cela peut sembler être un retour en arrière, mais les files d'instructions sont bien plus larges : de 42 micro-opérations pour le Pentium 3, on passe à 120 micro-opérations pour le Pentium 4. Et vu la longueur du pipeline, qui fait qu'il y a plus d'instructions en vol, c'était une nécessité. Mais cela n'aurait pas été possible en utilisant des stations de réservation, pour des raisons de consommation électrique et/ou de budget en transistors, ce qui fait que passer à une file de micro-opération été la solution retenue.
Pour les accès mémoire, le Pentium 4 utilisait donc une file de µops mémoire unique, couplée à une file d'écriture (non-représentée sur les schémas qui suivent). La file d'écriture du Pentium 4 était de 24 écritures maximum et gérait le ''Store-to-load forwarding''. Le processeur pouvait émettre une lecture et une écriture à chaque cycle. Il y avait un port d'émission pour les lectures et un autre pour les écritures, tous deux ayant chacun leur propre unité de calcul d'adresse.
Le processeur contenait 3 ALU entières, 2 unités de calcul d'adresse et une FPU. La FPU était complétée par une unité pour faire des copies entre registres flottants, des opérations MOV. Pour les AGU, il y en avait une dédiée aux lectures, une autre pour les écritures. Le tout était relié aux ports d'émissions comme suit :
[[File:Ports d'émission du Pentium 4.png|centre|vignette|upright=2.5|Ports d'émission du Pentium 4]]
Le processeur utilisait deux réseaux de contournement séparés : un pour les opérations flottantes, un pour les opérations entières. Le réseau de contournement pour les opérations entières est aussi relié aux unités de calcul d'adresse. Jusque là, rien de surprenant, le chemin de données du processeur est assez classique.
[[File:Architettura Pentium 4.png|centre|vignette|upright=3|Microarchitecture du Pentium 4.]]
===Les unités de calcul entières du Pentium 4===
Sur le Pentium 4, les ALU entières étaient cadencées à une fréquence double de celle du processeur. Les ALU entières pouvaient exécuter deux micro-opérations par cycle, ce qui fait que les ports d'émissions reliés aux ALU devaient eux aussi fonctionner à double fréquence. Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU.
Précisons que seules les ALU entières étaient à double fréquence, pas le multiplieur, pas le ''barrel shifter''. Pour simplifier, nous allons parler d'additionneur plutôt que de l'ALU entière, ce qui sera plus proche de la réalité. Et l'implémentation de l'additionneur du Pentium 4 était très innovante. L'additionneur pouvait exécuter deux additions par cycle, même si les deux additions ont une dépendance. Mais n'allez pas croire que l'implémentation était intuitive, avec un additionneur 32 bit basique très rapide. Non seulement l'additionneur fonctionnait à double fréquence, mais il était aussi pipeliné, avec un système de contournement interne !
Les additionneurs étaient pipelinées, d'une manière très simple. Une addition 32 bits était découpée en trois étapes : deux additions de 16 bits, une dernière étape pour mettre à jour le registre d'état. Pour cela, chaque additionneur était composé de deux additionneur 16 bits chacune, placées l'une après l'autre, avec un registre de pipeline entre les deux. L'additionneur prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible, le second calculait les 16 bits de poids fort lors du second cycle. Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel.
Une addition se fait donc en deux étapes, sauf que c'est compensé par le fait que l'additionneur fonctionnait à une fréquence double de celle du processeur ! Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. Deux additions consécutives s'exécutaient donc en 1 cycle et demi, alors qu'on aurait cru au premier abord que cela prendrait seulement un cycle. Si on fait les calculs, on s'apercoit que le rythme de croisière est cependant proche de 2 additions par cycle, bien qu'inférieur. 3 additions consécutives se font en deux cycles, 5 additions en 3 cycles, 7 en 4 cycles, etc.
Et le Pentium 4 ajoutait un système de contournement interne à l'ALU. En clair, si une addition utilise le résultat de l'addition précédente, les deux peuvent s'exécuter en un cycle d'horloge et demi. Les 16 bits de poids faible de la première addition sont disponibles après un cycle ALU, ce qui permet de démarrer le calcul de la seconde addition au cycle suivant.
===Le ''replay pipeline''===
Le processeur est un processeur triple émission : il peut charger et décoder 3 µops par cycle, le ROB peut terminer 3 µops par cycle, etc. Pourtant, les ports d'émission peuvent émettre 6 instructions par cycle : 4 ports, dont deux à double fréquence. Une telle différence s'explique par l'usage d'un ''replay pipeline'', dont nous avons déjà parlé dans ce cours.
Pour rappel, le Pentium 4 suppose que les lectures font tous un succès de cache L1. Si une opération arithmétique utilise la donnée lue comme opérande, le processeur l'émet immédiatement, l'opérande sera disponible une fois l'opération en entrée de l'ALU entière. Mais s'il s'est trompé, le processeur ré-exécute l'instruction après un temps d'attente de quelques cycles, pour se caler sur la latence du cache L2. Et si il y a un défaut de cache L2, l’instruction attend encore. Cela demande de ré-exécuter des instructions émises à tord, ce qui fait que le processeur doit avoir la capacité d'exécution pour. Ce pourquoi le processeur peut émettre 6 µops dans les unités de calcul : 3 µops normales et 3 µops ré-exécutées.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les processeurs superscalaires
| prevText=Les processeurs superscalaires
| next=Les processeurs VLIW et EPIC
| nextText=Les processeurs VLIW et EPIC
}}
</noinclude>
kg1n33pb3izobr79tywhu6fl9bpn1tn
Wikilivres:Prise de décision/Modifier le contenue de l'espace Nouveautés
4
84498
772493
772254
2026-09-20T09:09:54Z
JackPotte
5426
/* Votes */
772493
wikitext
text/x-wiki
{{Prise de décision}}
==Présentation==
'''Le problème :'''
Actuellement, dans l'espace "Nouveautés" il y a de nombreux livres qui ne sont plus des nouveaux livres. Cette espace pourrait continuer à mettre en avant les nouveaux livres pendant 1 mois, mais aussi des livres non terminés mais particulièrement actifs. Ils pourraient y rester 1 an, 2 ans, ... tant qu'ils ne seront pas considérés comme terminés par leurs auteurs. Aucune modification technique ne sera nécessaire, juste un choix judicieux de livres.
'''La solution :'''
* a) Je vais donc supprimer le modèle nouveau livres pour les livres de plus d'un mois. [[Modèle:Nouveau livre|Modèle:Nouveau livre]]
* b) Je vais laisser le livre [[Chanter les psaumes|Chanter les psaumes]] comme exemple de livre non terminé, mais particulièrement actif.
.
Cette page [https://fr.wikibooks.org/wiki/Utilisateur:Xhungab '''Wikibook'''] propose la proposition mise en page.
Merci de m'avoir suivie jusqu'ici. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 9 septembre 2026 à 14:20 (CEST)
:Je pense que le problème principal de Wikilivres est un manque de dynamisme lié à une communauté relativement restreinte et relativement peu interactive.
:Par définition, une rubrique nouveauté doit faire l'objet d'une mise à jour régulière. Ce qui demande un investissement de la communauté avec des échanges et des propositions. Réorganiser cet espace est donc une manière de redynamiser notre projet.
:Il est important qu'une vie communautaire se développe dans notre projet. Merci donc aussi à toi @[[Utilisateur:Xhungab|Xhungab]] de t'investir dans tous ces changements ! [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 septembre 2026 à 11:53 (CEST)
::Merci pour ton aide.[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 16 septembre 2026 à 12:46 (CEST)
== Discussions ==
== Votes ==
Votez {{m|Pour}} / {{m|Contre}} / {{m|Neutre}}, avec vos arguments.
# {{Pour}} Très bonne proposition. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 9 septembre 2026 à 14:20 (CEST)
# {{Pour}} Je suis favorable à toute proposition qui permet de redynamiser le projet, tout en sachant que l'on peut toujours améliorer les modifications par la suite. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 septembre 2026 à 11:57 (CEST)
# {{Pour}} [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 20 septembre 2026 à 11:09 (CEST)
== Décision ==
.
jv8eywgjehy96gjm6grymrvs8067fxw
Mathc initiation/0070
0
84515
772412
772407
2026-09-19T14:47:34Z
Xhungab
23827
772412
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==L'échelon unitaire : u(n) = 1==
La transformée en Z de f(n) est :
F(z) = sum f(n) z^(-n), n=0 to infinity
La transformée en Z de u(n) est : z/(z-1)
sum u(n) z^(-n), n=0 to infinity u(n) = 1
sum (1) z^(-n), n=0 to infinity
sum (z^(-1))^n, n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (z^(-1))^n, n=0 to infinity = 1/(1-z^(-1)) z^(-1) = 1/z
= 1/(1-1/z)
= z/(z-1)
==La rampe : r(n) = n==
La transformée en Z de f(n) est :
F(z) = sum f(n) z^(-n), n=0 to infinity
La transformée en Z de r(n) est : z/(z-1)^2
Travaillons sur x :
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
sum n x^(n ), n=0 to infinity = x/(1-x)^2
Travaillons sur z : x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1))) ^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = z/(z-1)^2 f) Cela donne
==L'exponentiel : f(n) = a^n==
La transformée en Z de f(n) est : z/(z-a)
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (a z^(-1))^(n), n=0 to infinity = 1/(1-(a z^(-1))) z^(-1) = 1/z
= 1/(1-(a/z))
= z/(z-(a))
= z/(z- a)
{{AutoCat}}
qx9pquaem6lk52rs3q69y5vc230khzm
772414
772412
2026-09-19T15:09:22Z
Xhungab
23827
772414
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==L'échelon unitaire : u(n) = 1==
La transformée en Z de f(n) est :
F(z) = sum f(n) z^(-n), n=0 to infinity
La transformée en Z de u(n) est : z/(z-1)
sum u(n) z^(-n), n=0 to infinity u(n) = 1
sum (1) z^(-n), n=0 to infinity
sum (z^(-1))^n, n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (z^(-1))^n, n=0 to infinity = 1/(1-z^(-1)) z^(-1) = 1/z
= 1/(1-1/z)
= z/(z-1)
==La rampe : r(n) = n==
La transformée en Z de f(n) est :
F(z) = sum f(n) z^(-n), n=0 to infinity
La transformée en Z de r(n) est : z/(z-1)^2
Travaillons sur x :
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
sum n x^(n ), n=0 to infinity = x/(1-x)^2
Travaillons sur z : x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = z/(z-1)^2 f) Cela donne
==L'exponentiel : f(n) = a^n==
La transformée en Z de f(n) est : z/(z-a)
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (a z^(-1))^(n), n=0 to infinity = 1/(1-(a z^(-1))) z^(-1) = 1/z
= 1/(1-(a/z))
= z/(z-(a))
= z/(z- a)
{{AutoCat}}
0dqs3xrfyhqd34pqy86t7nnqcdin20b
772486
772414
2026-09-19T22:03:19Z
Xhungab
23827
772486
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==L'échelon unitaire : u(n) = 1==
'''La transformée en Z de u(n) est : U(z) = z/(z-1)'''
sum u(n) z^(-n), n=0 to infinity u(n) = 1
sum (1) z^(-n), n=0 to infinity
sum (z^(-1))^n, n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (z^(-1))^n, n=0 to infinity = 1/(1-z^(-1)) x = z^(-1)
= 1/(1-1/z) z^(-1) = 1/z
= '''z/(z-1)'''
==La rampe : r(n) = n==
'''La transformée en Z de r(n) est : R(z) = z/(z-1)^2'''
'''Travaillons sur x :'''
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
sum n x^(n ), n=0 to infinity = x/(1-x)^2
'''Travaillons sur z :''' x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = f) Cela donne
'''z/(z-1)^2'''
==Le carré : c(n) = n^2==
'''La transformée en Z de c(n) = n^2 est : C(z) = z(z+1)/(z-1)^3'''
Utilisons la transformé en Z de la rampe r(n) = n R(z) = z/(z-1)^2
Si nous utilisons la propriété de [[Mathc initiation/a588|la Multiplication par la variable d'évolution]] sur la transformé de la rampe nous obtenons :
x(n) = n r(n) alors Z[x(n)] = Z[n r(n)] = -z X'(z)
X'(z) = (z/(z-1)^2)' = -(z+1)/(z-1)^3
Z[n r(n)] = (-z) X'(z) = (-z) (-(z+1)/(z-1)^3)
Z[n r(n)] = '''z (z+1)/(z-1)^3 = C(n)'''
==L'exponentiel : f(n) = a^n==
'''La transformée en Z de f(n) est : F(z) = z/(z-a)'''
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (a z^(-1))^(n), n=0 to infinity = 1/(1-(a z^(-1))) Remplaçons x par (a z^(-1))
= 1/(1-(a/z)) (a z^(-1)) = (a/z)
= z/(z-(a))
= '''z/(z- a)'''
{{AutoCat}}
gxibmjqyrslmi812gwqlceadjhj30cw
772490
772486
2026-09-19T22:19:45Z
Xhungab
23827
772490
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==L'échelon unitaire : u(n) = 1==
'''La transformée en Z de u(n) est : U(z) = z/(z-1)'''
sum u(n) z^(-n), n=0 to infinity u(n) = 1
sum (1) z^(-n), n=0 to infinity
sum (z^(-1))^n, n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (z^(-1))^n, n=0 to infinity = 1/(1-z^(-1)) x = z^(-1)
= 1/(1-1/z) z^(-1) = 1/z
= '''z/(z-1)'''
==La rampe : r(n) = n==
'''La transformée en Z de r(n) est : R(z) = z/(z-1)^2'''
'''Travaillons sur x :'''
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
sum n x^(n ), n=0 to infinity = x/(1-x)^2
'''Travaillons sur z :''' x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = f) Cela donne
'''z/(z-1)^2'''
==Le carré : c(n) = n^2==
'''La transformée en Z de c(n) = n^2 est : C(z) = z(z+1)/(z-1)^3'''
Utilisons la transformé en Z de la rampe r(n) = n R(z) = z/(z-1)^2
Si nous utilisons la propriété de [[Mathc initiation/a588|la Multiplication par la variable d'évolution]] sur la transformé de la rampe nous obtenons :
x(n) = n r(n) alors Z[x(n)] = Z[n r(n)] = -z X'(z)
X'(z) = (z/(z-1)^2)' = -(z+1)/(z-1)^3
Z[n r(n)] = (-z) X'(z) = (-z) (-(z+1)/(z-1)^3)
Z[n r(n)] = '''z (z+1)/(z-1)^3 = C(n)'''
==L'exponentiel : f(n) = a^n==
'''La transformée en Z de f(n) est : F(z) = z/(z-a)'''
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (a z^(-1))^(n), n=0 to infinity = 1/(1-(a z^(-1))) Remplaçons x par (a z^(-1))
= 1/(1-(a/z)) (a z^(-1)) = (a/z)
= z/(z-(a))
= '''z/(z- a)'''
{{AutoCat}}
87b08lw7nujlxgnyl2np4ybtc26e5j6
772492
772490
2026-09-20T09:05:20Z
Xhungab
23827
772492
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a582#Transformées en Z usuelles, des signaux causales discrets :| Sommaire]]
==L'échelon unitaire : u(n) = 1==
'''La transformée en Z de u(n) est : U(z) = z/(z-1)'''
sum u(n) z^(-n), n=0 to infinity u(n) = 1
sum (1) z^(-n), n=0 to infinity
sum (z^(-1))^n, n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (z^(-1))^n, n=0 to infinity = 1/(1-z^(-1)) x = z^(-1)
= 1/(1-1/z) z^(-1) = 1/z
= '''z/(z-1)'''
==La rampe : r(n) = n==
'''La transformée en Z de r(n) est : R(z) = z/(z-1)^2'''
'''Travaillons sur x :'''
sum x^(n), n=0 to infinity = 1/(1-x) |x| < 1
sum x'^(n), n=0 to infinity = (1/(1-x))' Dérivons
sum n x^(n-1), n=0 to infinity = 1/(1-x)^2
(x) [sum n x^(n-1), n=0 to infinity = 1/(1-x)^2] Multiplions par x
(*) sum n x^(n ), n=0 to infinity = x/(1-x)^2
'''Travaillons sur z :''' x -> z^(-1)
sum n (z^(-1))^(n), n=0 to infinity = (*) Remplaçons x par (z^(-1))
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = a) Simplifions (z^(-1))^(n)
(z^(-1))/(1-(z^(-1)))^2
sum n z^(-n)), n=0 to infinity = b) Simplifions (z^(-1)) = (1/z)
(1/z)/(1-(1/z))^2
sum n z^(-n)), n=0 to infinity = c) Développons les (1/z)
1/[((z-1)/z))^2 z]
sum n z^(-n)), n=0 to infinity = d) Distribuons les carrés
1/[(z-1)^2/z^2) z]
sum n z^(-n)), n=0 to infinity = e) Simplifions le z^2
z^2 /[(z-1))^2 z]
sum n z^(-n)), n=0 to infinity = f) Cela donne
'''z/(z-1)^2'''
==Le carré : c(n) = n^2==
'''La transformée en Z de c(n) = n^2 est : C(z) = z(z+1)/(z-1)^3'''
Utilisons la transformé en Z de la rampe r(n) = n R(z) = z/(z-1)^2
Si nous utilisons la propriété de [[Mathc initiation/a588|la Multiplication par la variable d'évolution]] sur la transformé de la rampe nous obtenons :
x(n) = n r(n) alors Z[x(n)] = Z[n r(n)] = -z X'(z)
X'(z) = (z/(z-1)^2)' = -(z+1)/(z-1)^3
Z[n r(n)] = (-z) X'(z) = (-z) (-(z+1)/(z-1)^3)
Z[n r(n)] = '''z (z+1)/(z-1)^3 = C(n)'''
==L'exponentiel : f(n) = a^n==
'''La transformée en Z de f(n) est : F(z) = z/(z-a)'''
sum f(n) z^(-n), n=0 to infinity f(n) = a^n
sum (a^n) z^(-n), n=0 to infinity
sum (a^n) (z^(-1))^(n), n=0 to infinity
sum (a z^(-1))^(n), n=0 to infinity
Remarque : sum x^n, n=0 to infinity = 1/(1-x) |x|<1
sum (a z^(-1))^(n), n=0 to infinity = 1/(1-(a z^(-1))) Remplaçons x par (a z^(-1))
= 1/(1-(a/z)) (a z^(-1)) = (a/z)
= z/(z-(a))
= '''z/(z- a)'''
{{AutoCat}}
f3ba9d5dvugxxg9ler8jdyjk8iggvyv